All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 00/11] hw/riscv: Add K230 SPI, QSPI, IDMA and XIP support
@ 2026-07-26 12:28 ` Kangjie Huang
  0 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:27 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Hi,

The k230 machine currently models the C908 core, CLINT, PLIC, watchdogs
and UARTs, but cannot yet model the real boot path. This series adds the
Kendryte K230 DesignWare SSI controllers and their machine integration
as groundwork for full boot support, enabling U-Boot to boot from SPI
NOR through standard PIO, QSPI IDMA, and the XIP read window.

K230 contains three SSI instances with different capabilities. The SDK
numbers them according to the address map: spi0 is the SPI-OPI instance,
while spi1 and spi2 are QSPI0 and QSPI1. The series models the register
contract documented in the K230 Technical Reference Manual V0.3.1
(2024-11-18), standard FIFO-backed SPI transfers, interrupt routing,
Dual/Quad SDR transfers, SPI NOR attachment, synchronous internal DMA,
the HI_SYS SSI_CTRL wrapper, and the spi0 XIP read window.

The implementation is split so that the register model and machine
integration come first, followed by the PIO data path, interrupt support,
enhanced transfers, flash integration, IDMA, HI_SYS, and XIP. A single
K230 SSI qtest binary grows with the series and exercises each layer when
it becomes available.

The final patch adds trace events covering register accesses (excluding
DR, whose per-frame traffic would drown out everything else during U-Boot
and Linux SPI NOR transfers), transaction boundaries, interrupt levels,
IDMA start/completion/error, and XIP reads. The trace points are placed
in a dedicated patch at the end of the series so that the functional
patches stay focused on behaviour and the trace surface can be reviewed
in one place. The events only observe model behaviour and do not change
it.

Testing:

* Built qemu-system-riscv64 at every commit with the riscv64-softmmu
  configuration.
* Ran the cumulative K230 SSI qtest after every commit that provides the
  test target. The final revision passes all ten scenarios:
  register-contract, pio-data-path, interrupt-controller, plic-routing,
  qspi-config, spi-nor, qspi-sdr, idma, hi-sys, and xip-read-window.
* Standard SPI: U-Boot detected W25Q256, loaded OpenSBI, Linux, initrd,
  and DTB with sf read, and reached the Linux initramfs shell.
* Quad SPI: with spi0 configured for 4-bit transfers, U-Boot erased a
  64 KiB sector, wrote and read back 256 bytes successfully, loaded all
  boot payloads from QSPI flash, and reached the Linux initramfs shell.
* XIP: U-Boot read the OpenSBI uImage header from 0xc0000000, verified
  its checksum, and reached the Linux initramfs shell through bootm.
* git diff --check reports no whitespace errors.
* checkpatch reports no errors.

Kangjie Huang (11):
  hw/ssi: Add K230 DesignWare SSI register model
  hw/riscv/k230: Instantiate K230 SSI controllers
  hw/ssi: Implement K230 SSI FIFO and standard PIO transfers
  hw/ssi: Add K230 SSI interrupt controller
  hw/riscv: Route K230 SSI IRQs to the PLIC
  hw/ssi: Implement K230 enhanced QSPI transfers
  hw/riscv/k230: Attach SPI NOR flash to spi0
  hw/ssi: Implement K230 SSI internal DMA transfers
  hw/misc: Add K230 HI_SYS SSI control
  hw/ssi: Add K230 SSI XIP read window
  hw/ssi: Add trace events for K230 DesignWare SSI

 docs/system/riscv/k230.rst     |    4 +
 hw/misc/k230_hi_sys.c          |  169 +++
 hw/misc/meson.build            |    1 +
 hw/riscv/Kconfig               |    2 +
 hw/riscv/k230.c                |  165 ++-
 hw/ssi/Kconfig                 |    4 +
 hw/ssi/k230_dw_ssi.c           | 1754 ++++++++++++++++++++++++++++++++
 hw/ssi/meson.build             |    1 +
 hw/ssi/trace-events            |   11 +
 include/hw/misc/k230_hi_sys.h  |   55 +
 include/hw/riscv/k230.h        |    9 +
 include/hw/ssi/k230_dw_ssi.h   |  111 ++
 tests/qtest/k230-dw-ssi-test.c |  917 +++++++++++++++++
 tests/qtest/meson.build        |    4 +-
 14 files changed, 3191 insertions(+), 16 deletions(-)
 create mode 100644 hw/misc/k230_hi_sys.c
 create mode 100644 hw/ssi/k230_dw_ssi.c
 create mode 100644 include/hw/misc/k230_hi_sys.h
 create mode 100644 include/hw/ssi/k230_dw_ssi.h
 create mode 100644 tests/qtest/k230-dw-ssi-test.c


base-commit: f893c46c3931b3684d235d221bf8b7844ddbf1d7
-- 
2.43.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 00/11] hw/riscv: Add K230 SPI, QSPI, IDMA and XIP support
@ 2026-07-26 12:28 ` Kangjie Huang
  0 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Hi,

The k230 machine currently models the C908 core, CLINT, PLIC, watchdogs
and UARTs, but cannot yet model the real boot path. This series adds the
Kendryte K230 DesignWare SSI controllers and their machine integration
as groundwork for full boot support, enabling U-Boot to boot from SPI
NOR through standard PIO, QSPI IDMA, and the XIP read window.

K230 contains three SSI instances with different capabilities. The SDK
numbers them according to the address map: spi0 is the SPI-OPI instance,
while spi1 and spi2 are QSPI0 and QSPI1. The series models the register
contract documented in the K230 Technical Reference Manual V0.3.1
(2024-11-18), standard FIFO-backed SPI transfers, interrupt routing,
Dual/Quad SDR transfers, SPI NOR attachment, synchronous internal DMA,
the HI_SYS SSI_CTRL wrapper, and the spi0 XIP read window.

The implementation is split so that the register model and machine
integration come first, followed by the PIO data path, interrupt support,
enhanced transfers, flash integration, IDMA, HI_SYS, and XIP. A single
K230 SSI qtest binary grows with the series and exercises each layer when
it becomes available.

The final patch adds trace events covering register accesses (excluding
DR, whose per-frame traffic would drown out everything else during U-Boot
and Linux SPI NOR transfers), transaction boundaries, interrupt levels,
IDMA start/completion/error, and XIP reads. The trace points are placed
in a dedicated patch at the end of the series so that the functional
patches stay focused on behaviour and the trace surface can be reviewed
in one place. The events only observe model behaviour and do not change
it.

Testing:

* Built qemu-system-riscv64 at every commit with the riscv64-softmmu
  configuration.
* Ran the cumulative K230 SSI qtest after every commit that provides the
  test target. The final revision passes all ten scenarios:
  register-contract, pio-data-path, interrupt-controller, plic-routing,
  qspi-config, spi-nor, qspi-sdr, idma, hi-sys, and xip-read-window.
* Standard SPI: U-Boot detected W25Q256, loaded OpenSBI, Linux, initrd,
  and DTB with sf read, and reached the Linux initramfs shell.
* Quad SPI: with spi0 configured for 4-bit transfers, U-Boot erased a
  64 KiB sector, wrote and read back 256 bytes successfully, loaded all
  boot payloads from QSPI flash, and reached the Linux initramfs shell.
* XIP: U-Boot read the OpenSBI uImage header from 0xc0000000, verified
  its checksum, and reached the Linux initramfs shell through bootm.
* git diff --check reports no whitespace errors.
* checkpatch reports no errors.

Kangjie Huang (11):
  hw/ssi: Add K230 DesignWare SSI register model
  hw/riscv/k230: Instantiate K230 SSI controllers
  hw/ssi: Implement K230 SSI FIFO and standard PIO transfers
  hw/ssi: Add K230 SSI interrupt controller
  hw/riscv: Route K230 SSI IRQs to the PLIC
  hw/ssi: Implement K230 enhanced QSPI transfers
  hw/riscv/k230: Attach SPI NOR flash to spi0
  hw/ssi: Implement K230 SSI internal DMA transfers
  hw/misc: Add K230 HI_SYS SSI control
  hw/ssi: Add K230 SSI XIP read window
  hw/ssi: Add trace events for K230 DesignWare SSI

 docs/system/riscv/k230.rst     |    4 +
 hw/misc/k230_hi_sys.c          |  169 +++
 hw/misc/meson.build            |    1 +
 hw/riscv/Kconfig               |    2 +
 hw/riscv/k230.c                |  165 ++-
 hw/ssi/Kconfig                 |    4 +
 hw/ssi/k230_dw_ssi.c           | 1754 ++++++++++++++++++++++++++++++++
 hw/ssi/meson.build             |    1 +
 hw/ssi/trace-events            |   11 +
 include/hw/misc/k230_hi_sys.h  |   55 +
 include/hw/riscv/k230.h        |    9 +
 include/hw/ssi/k230_dw_ssi.h   |  111 ++
 tests/qtest/k230-dw-ssi-test.c |  917 +++++++++++++++++
 tests/qtest/meson.build        |    4 +-
 14 files changed, 3191 insertions(+), 16 deletions(-)
 create mode 100644 hw/misc/k230_hi_sys.c
 create mode 100644 hw/ssi/k230_dw_ssi.c
 create mode 100644 include/hw/misc/k230_hi_sys.h
 create mode 100644 include/hw/ssi/k230_dw_ssi.h
 create mode 100644 tests/qtest/k230-dw-ssi-test.c


base-commit: f893c46c3931b3684d235d221bf8b7844ddbf1d7
-- 
2.43.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 01/11] hw/ssi: Add K230 DesignWare SSI register model
  2026-07-26 12:28 ` Kangjie Huang
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Add a SysBus model for the K230 DesignWare SSI controllers.

Implement the register layout, reset values, writable masks, FIFO state,
chip-select GPIOs, and the MMIO regions needed by the K230 machine,
based on the K230 Technical Reference Manual. The controller is not
instantiated by this patch.

Signed-off-by: Kangjie Huang <flamboyant.h.01@gmail.com>
---
 hw/ssi/Kconfig               |   4 +
 hw/ssi/k230_dw_ssi.c         | 795 +++++++++++++++++++++++++++++++++++
 hw/ssi/meson.build           |   1 +
 include/hw/ssi/k230_dw_ssi.h |  50 +++
 4 files changed, 850 insertions(+)
 create mode 100644 hw/ssi/k230_dw_ssi.c
 create mode 100644 include/hw/ssi/k230_dw_ssi.h

diff --git a/hw/ssi/Kconfig b/hw/ssi/Kconfig
index 1bd56463c1..05ff0e89ee 100644
--- a/hw/ssi/Kconfig
+++ b/hw/ssi/Kconfig
@@ -6,6 +6,10 @@ config SIFIVE_SPI
     bool
     select SSI
 
+config K230_DW_SSI
+    bool
+    select SSI
+
 config SSI
     bool
 
diff --git a/hw/ssi/k230_dw_ssi.c b/hw/ssi/k230_dw_ssi.c
new file mode 100644
index 0000000000..d0dcad4312
--- /dev/null
+++ b/hw/ssi/k230_dw_ssi.c
@@ -0,0 +1,795 @@
+/*
+ * Kendryte K230 DesignWare SSI
+ *
+ * Copyright (c) 2026 Kangjie Huang <flamboyant.h.01@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Emulates the DesignWare SSI controllers documented by the K230
+ * Technical Reference Manual, including standard SPI, Dual/Quad SDR,
+ * internal DMA, and the XIP read window.
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18):
+ * https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
+ *
+ * For more information, see <https://www.kendryte.com/en/proDetail/230>
+ */
+
+#include "qemu/osdep.h"
+#include "hw/core/registerfields.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/core/irq.h"
+#include "hw/ssi/k230_dw_ssi.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qemu/bitops.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+
+#define K230_DW_SSI_FIFO_CAPACITY 256
+
+#define K230_DW_SSI_CTRLR0_RESET            0x00004007
+#define K230_DW_SSI_SR_RESET                0x00000006
+#define K230_DW_SSI_IMR_RESET               0x0000003f
+#define K230_DW_SSI_IDR_RESET               0xa1b2c3d5
+#define K230_DW_SSI_SPI_CTRLR0_SPI_RESET    0x04000200
+#define K230_DW_SSI_SPI_CTRLR0_FMC_RESET    0x28000200
+#define K230_DW_SSI_AXIAWLEN_RESET          0x00000700
+#define K230_DW_SSI_AXIARLEN_RESET          0x00000700
+#define K230_DW_SSI_VERSION                 0x3130332a
+
+REG32(CTRLR0, 0x000)
+    FIELD(CTRLR0, DFS, 0, 5)
+    FIELD(CTRLR0, FRF, 6, 2)
+    FIELD(CTRLR0, SCPH, 8, 1)
+    FIELD(CTRLR0, SCPOL, 9, 1)
+    FIELD(CTRLR0, TMOD, 10, 2)
+    FIELD(CTRLR0, SLV_OE, 12, 1)
+    FIELD(CTRLR0, SRL, 13, 1)
+    FIELD(CTRLR0, SSTE, 14, 1)
+    FIELD(CTRLR0, CFS, 16, 4)
+    FIELD(CTRLR0, SPI_FRF, 22, 2)
+    FIELD(CTRLR0, SPI_HYPERBUS_EN, 24, 1)
+    FIELD(CTRLR0, SPI_DWS_EN, 25, 1)
+REG32(CTRLR1, 0x004)
+    FIELD(CTRLR1, NDF, 0, 16)
+REG32(SSIENR, 0x008)
+    FIELD(SSIENR, SSIC_EN, 0, 1)
+REG32(MWCR, 0x00c)
+    FIELD(MWCR, MWMOD, 0, 1)
+    FIELD(MWCR, MDD, 1, 1)
+    FIELD(MWCR, MHS, 2, 1)
+REG32(SER, 0x010)
+REG32(BAUDR, 0x014)
+    FIELD(BAUDR, SCKDV, 1, 15)
+REG32(TXFTLR, 0x018)
+    FIELD(TXFTLR, TFT, 0, 8)
+    FIELD(TXFTLR, TXFTHR, 16, 11)
+REG32(RXFTLR, 0x01c)
+    FIELD(RXFTLR, RFT, 0, 8)
+REG32(TXFLR, 0x020)
+    FIELD(TXFLR, TXTFL, 0, 9)
+REG32(RXFLR, 0x024)
+    FIELD(RXFLR, RXTFL, 0, 9)
+REG32(SR, 0x028)
+    FIELD(SR, CMPLTD_DF, 15, 17)
+    FIELD(SR, DCOL, 6, 1)
+    FIELD(SR, TXE, 5, 1)
+    FIELD(SR, BUSY, 0, 1)
+    FIELD(SR, TFNF, 1, 1)
+    FIELD(SR, TFE, 2, 1)
+    FIELD(SR, RFNE, 3, 1)
+    FIELD(SR, RFF, 4, 1)
+REG32(IMR, 0x02c)
+    FIELD(IMR, DONEM, 11, 1)
+    FIELD(IMR, SPITEM, 10, 1)
+    FIELD(IMR, AXIEM, 8, 1)
+    FIELD(IMR, TXUIM, 7, 1)
+    FIELD(IMR, XRXOIM, 6, 1)
+    FIELD(IMR, MSTIM, 5, 1)
+    FIELD(IMR, RXOIM, 3, 1)
+    FIELD(IMR, RXUIM, 2, 1)
+    FIELD(IMR, TXOIM, 1, 1)
+    FIELD(IMR, TXEIM, 0, 1)
+    FIELD(IMR, RXFIM, 4, 1)
+REG32(ISR, 0x030)
+    FIELD(ISR, DONES, 11, 1)
+    FIELD(ISR, SPITES, 10, 1)
+    FIELD(ISR, AXIES, 8, 1)
+    FIELD(ISR, TXUIS, 7, 1)
+    FIELD(ISR, XRXOIS, 6, 1)
+    FIELD(ISR, MSTIS, 5, 1)
+    FIELD(ISR, RXOIS, 3, 1)
+    FIELD(ISR, RXUIS, 2, 1)
+    FIELD(ISR, TXOIS, 1, 1)
+    FIELD(ISR, TXEIS, 0, 1)
+    FIELD(ISR, RXFIS, 4, 1)
+REG32(RISR, 0x034)
+    FIELD(RISR, DONER, 11, 1)
+    FIELD(RISR, SPITER, 10, 1)
+    FIELD(RISR, AXIER, 8, 1)
+    FIELD(RISR, TXUIR, 7, 1)
+    FIELD(RISR, XRXOIR, 6, 1)
+    FIELD(RISR, MSTIR, 5, 1)
+    FIELD(RISR, RXOIR, 3, 1)
+    FIELD(RISR, RXUIR, 2, 1)
+    FIELD(RISR, TXOIR, 1, 1)
+    FIELD(RISR, TXEIR, 0, 1)
+    FIELD(RISR, RXFIR, 4, 1)
+REG32(TXEICR, 0x038)
+    FIELD(TXEICR, TXEICR, 0, 1)
+REG32(RXOICR, 0x03c)
+    FIELD(RXOICR, RXOICR, 0, 1)
+REG32(RXUICR, 0x040)
+    FIELD(RXUICR, RXUICR, 0, 1)
+REG32(MSTICR, 0x044)
+    FIELD(MSTICR, MSTICR, 0, 1)
+REG32(ICR, 0x048)
+    FIELD(ICR, ICR, 0, 1)
+REG32(DMACR, 0x04c)
+    FIELD(DMACR, RDMAE, 0, 1)
+    FIELD(DMACR, TDMAE, 1, 1)
+    FIELD(DMACR, IDMAE, 2, 1)
+    FIELD(DMACR, ATW, 3, 2)
+    FIELD(DMACR, AINC, 6, 1)
+    FIELD(DMACR, ACACHE, 8, 4)
+    FIELD(DMACR, APROT, 12, 3)
+    FIELD(DMACR, AID, 15, 4)
+REG32(AXIAWLEN, 0x050)
+    FIELD(AXIAWLEN, AWLEN, 8, 8)
+REG32(AXIARLEN, 0x054)
+    FIELD(AXIARLEN, ARLEN, 8, 8)
+REG32(IDR, 0x058)
+    FIELD(IDR, IDCODE, 0, 32)
+REG32(SSIC_VERSION_ID, 0x05c)
+    FIELD(SSIC_VERSION_ID, VERSION_ID, 0, 32)
+REG32(DR0, 0x060)
+REG32(DR_END, 0x0ec)
+REG32(RX_SAMPLE_DELAY, 0x0f0)
+    FIELD(RX_SAMPLE_DELAY, RSD, 0, 8)
+    FIELD(RX_SAMPLE_DELAY, SE, 16, 1)
+REG32(SPI_CTRLR0, 0x0f4)
+    FIELD(SPI_CTRLR0, CLK_STRETCH_EN, 30, 1)
+    FIELD(SPI_CTRLR0, XIP_PREFETCH_EN, 29, 1)
+    FIELD(SPI_CTRLR0, XIP_MBL, 26, 2)
+    FIELD(SPI_CTRLR0, SPI_RXDS_SIG_EN, 25, 1)
+    FIELD(SPI_CTRLR0, SPI_DM_EN, 24, 1)
+    FIELD(SPI_CTRLR0, SSIC_XIP_CONT_XFER_EN, 21, 1)
+    FIELD(SPI_CTRLR0, XIP_INST_EN, 20, 1)
+    FIELD(SPI_CTRLR0, XIP_DFS_HC, 19, 1)
+    FIELD(SPI_CTRLR0, SPI_RXDS_EN, 18, 1)
+    FIELD(SPI_CTRLR0, INST_DDR_EN, 17, 1)
+    FIELD(SPI_CTRLR0, SPI_DDR_EN, 16, 1)
+    FIELD(SPI_CTRLR0, WAIT_CYCLES, 11, 5)
+    FIELD(SPI_CTRLR0, INST_L, 8, 2)
+    FIELD(SPI_CTRLR0, XIP_MD_BIT_EN, 7, 1)
+    FIELD(SPI_CTRLR0, ADDR_L, 2, 4)
+    FIELD(SPI_CTRLR0, TRANS_TYPE, 0, 2)
+REG32(DDR_DRIVE_EDGE, 0x0f8)
+    FIELD(DDR_DRIVE_EDGE, TDE, 0, 8)
+REG32(XIP_MODE_BITS, 0x0fc)
+    FIELD(XIP_MODE_BITS, XIP_MD_BITS, 0, 16)
+REG32(XIP_INCR_INST, 0x100)
+    FIELD(XIP_INCR_INST, INCR_INST, 0, 16)
+REG32(XIP_WRAP_INST, 0x104)
+    FIELD(XIP_WRAP_INST, WRAP_INST, 0, 16)
+REG32(XIP_CTRL, 0x108)
+REG32(XIP_SER, 0x10c)
+REG32(XRXOICR, 0x110)
+REG32(XIP_CNT_TIME_OUT, 0x114)
+REG32(SPI_CTRLR1, 0x118)
+REG32(SPITECR, 0x11c)
+REG32(SPIDR, 0x120)
+    FIELD(SPIDR, SPI_INST, 0, 16)
+REG32(SPIAR, 0x124)
+    FIELD(SPIAR, SDAR, 0, 32)
+REG32(AXIAR0, 0x128)
+    FIELD(AXIAR0, AXIAR_0_31, 0, 32)
+REG32(AXIAR1, 0x12c)
+    FIELD(AXIAR1, AXIAR_32_63, 0, 32)
+REG32(AXIECR, 0x130)
+    FIELD(AXIECR, AXIECR, 0, 1)
+REG32(DONECR, 0x134)
+    FIELD(DONECR, DONECR, 0, 1)
+REG32(RSVD_138, 0x138)
+REG32(RSVD_13C, 0x13c)
+REG32(XIP_WRITE_INCR_INST, 0x140)
+REG32(XIP_WRITE_WRAP_INST, 0x144)
+REG32(XIP_WRITE_CTRL, 0x148)
+
+#define K230_DW_SSI_CTRLR0_WRITABLE_MASK \
+    (R_CTRLR0_DFS_MASK | \
+     R_CTRLR0_SCPH_MASK | \
+     R_CTRLR0_SCPOL_MASK | \
+     R_CTRLR0_TMOD_MASK | \
+     R_CTRLR0_SLV_OE_MASK | \
+     R_CTRLR0_SRL_MASK | \
+     R_CTRLR0_SSTE_MASK | \
+     R_CTRLR0_CFS_MASK | \
+     R_CTRLR0_SPI_FRF_MASK | \
+     R_CTRLR0_SPI_HYPERBUS_EN_MASK)
+
+#define K230_DW_SSI_CTRLR1_WRITABLE_MASK R_CTRLR1_NDF_MASK
+#define K230_DW_SSI_MWCR_WRITABLE_MASK \
+    (R_MWCR_MWMOD_MASK | R_MWCR_MDD_MASK)
+#define K230_DW_SSI_BAUDR_WRITABLE_MASK R_BAUDR_SCKDV_MASK
+#define K230_DW_SSI_TXFTLR_WRITABLE_MASK \
+    (R_TXFTLR_TFT_MASK | R_TXFTLR_TXFTHR_MASK)
+#define K230_DW_SSI_RXFTLR_WRITABLE_MASK R_RXFTLR_RFT_MASK
+#define K230_DW_SSI_DMACR_WRITABLE_MASK \
+    (R_DMACR_IDMAE_MASK | R_DMACR_ATW_MASK | R_DMACR_AINC_MASK | \
+     R_DMACR_ACACHE_MASK | R_DMACR_APROT_MASK | R_DMACR_AID_MASK)
+#define K230_DW_SSI_AXIAWLEN_WRITABLE_MASK R_AXIAWLEN_AWLEN_MASK
+#define K230_DW_SSI_AXIARLEN_WRITABLE_MASK R_AXIARLEN_ARLEN_MASK
+#define K230_DW_SSI_IMR_WRITABLE_MASK \
+    (R_IMR_TXEIM_MASK | R_IMR_TXOIM_MASK | R_IMR_RXUIM_MASK | \
+     R_IMR_RXOIM_MASK | R_IMR_RXFIM_MASK | R_IMR_MSTIM_MASK | \
+     R_IMR_TXUIM_MASK | R_IMR_AXIEM_MASK | R_IMR_DONEM_MASK)
+#define K230_DW_SSI_RX_SAMPLE_DELAY_WRITABLE_MASK \
+    (R_RX_SAMPLE_DELAY_RSD_MASK | R_RX_SAMPLE_DELAY_SE_MASK)
+#define K230_DW_SSI_SPI_CTRLR0_WRITABLE_MASK \
+    (R_SPI_CTRLR0_CLK_STRETCH_EN_MASK | \
+     R_SPI_CTRLR0_XIP_PREFETCH_EN_MASK | \
+     R_SPI_CTRLR0_XIP_MBL_MASK | \
+     R_SPI_CTRLR0_SPI_RXDS_SIG_EN_MASK | \
+     R_SPI_CTRLR0_SPI_DM_EN_MASK | \
+     R_SPI_CTRLR0_SSIC_XIP_CONT_XFER_EN_MASK | \
+     R_SPI_CTRLR0_XIP_INST_EN_MASK | \
+     R_SPI_CTRLR0_XIP_DFS_HC_MASK | \
+     R_SPI_CTRLR0_INST_DDR_EN_MASK | \
+     R_SPI_CTRLR0_SPI_DDR_EN_MASK | \
+     R_SPI_CTRLR0_SPI_RXDS_EN_MASK | \
+     R_SPI_CTRLR0_WAIT_CYCLES_MASK | \
+     R_SPI_CTRLR0_INST_L_MASK | \
+     R_SPI_CTRLR0_XIP_MD_BIT_EN_MASK | \
+     R_SPI_CTRLR0_ADDR_L_MASK | \
+     R_SPI_CTRLR0_TRANS_TYPE_MASK)
+#define K230_DW_SSI_DDR_DRIVE_EDGE_WRITABLE_MASK \
+    R_DDR_DRIVE_EDGE_TDE_MASK
+#define K230_DW_SSI_XIP_MODE_BITS_WRITABLE_MASK \
+    R_XIP_MODE_BITS_XIP_MD_BITS_MASK
+#define K230_DW_SSI_XIP_INCR_INST_WRITABLE_MASK \
+    R_XIP_INCR_INST_INCR_INST_MASK
+#define K230_DW_SSI_XIP_WRAP_INST_WRITABLE_MASK \
+    R_XIP_WRAP_INST_WRAP_INST_MASK
+#define K230_DW_SSI_SPIDR_WRITABLE_MASK R_SPIDR_SPI_INST_MASK
+#define K230_DW_SSI_SPIAR_WRITABLE_MASK R_SPIAR_SDAR_MASK
+#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 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_frame_masked(K230DwSsiState *s)
+{
+    unsigned int bits = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, DFS) + 1;
+
+    return bits == 32 ? UINT32_MAX : MAKE_64BIT_MASK(0, bits);
+}
+
+
+static bool k230_dw_ssi_enabled(K230DwSsiState *s)
+{
+    return FIELD_EX32(s->regs[R_SSIENR], SSIENR, SSIC_EN);
+}
+
+static void k230_dw_ssi_deselect(K230DwSsiState *s)
+{
+    if (s->active_cs < 0) {
+        return;
+    }
+
+    qemu_irq_raise(s->cs_lines[s->active_cs]);
+    s->active_cs = -1;
+}
+
+static void k230_dw_ssi_select(K230DwSsiState *s, unsigned cs)
+{
+    if (cs >= s->num_cs) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: invalid chip select %u\n",
+                      DEVICE(s)->canonical_path, cs);
+        k230_dw_ssi_deselect(s);
+        return;
+    }
+
+    if (s->active_cs == cs) {
+        return;
+    }
+
+    k230_dw_ssi_deselect(s);
+    qemu_irq_lower(s->cs_lines[cs]);
+    s->active_cs = cs;
+}
+
+static void k230_dw_ssi_update_cs(K230DwSsiState *s)
+{
+    uint32_t ser = s->regs[R_SER];
+
+    if (!k230_dw_ssi_enabled(s) || !ser) {
+        k230_dw_ssi_deselect(s);
+        return;
+    }
+
+    if (ser & (ser - 1)) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: multiple chip selects enabled: 0x%x\n",
+                      DEVICE(s)->canonical_path, ser);
+        k230_dw_ssi_deselect(s);
+        return;
+    }
+
+    k230_dw_ssi_select(s, ctz32(ser));
+}
+
+static void k230_dw_ssi_abort_transfer(K230DwSsiState *s)
+{
+    k230_dw_ssi_deselect(s);
+    fifo32_reset(&s->tx_fifo);
+    fifo32_reset(&s->rx_fifo);
+}
+
+static uint32_t k230_dw_ssi_status(K230DwSsiState *s)
+{
+    uint32_t tx_used = fifo32_num_used(&s->tx_fifo);
+    uint32_t rx_used = fifo32_num_used(&s->rx_fifo);
+    uint32_t sr = 0;
+
+    sr = FIELD_DP32(sr, SR, TFNF, tx_used < K230_DW_SSI_FIFO_CAPACITY);
+    sr = FIELD_DP32(sr, SR, TFE, tx_used == 0);
+    sr = FIELD_DP32(sr, SR, RFNE, rx_used != 0);
+    sr = FIELD_DP32(sr, SR, RFF,
+                    rx_used == K230_DW_SSI_FIFO_CAPACITY);
+
+    return sr;
+}
+
+static void k230_dw_ssi_run_transfer(K230DwSsiState *s);
+
+static void k230_dw_ssi_push_tx(K230DwSsiState *s, uint32_t tx)
+{
+    if (!k230_dw_ssi_enabled(s) || s->active_cs < 0) {
+        return;
+    }
+
+    if (fifo32_is_full(&s->tx_fifo)) {
+        return;
+    }
+
+    fifo32_push(&s->tx_fifo, tx & k230_dw_ssi_frame_masked(s));
+    k230_dw_ssi_run_transfer(s);
+}
+
+
+static void k230_dw_ssi_run_transfer(K230DwSsiState *s)
+{
+}
+
+
+
+static bool k230_dw_ssi_is_dr(hwaddr addr)
+{
+    return addr >= A_DR0 && addr <= A_DR_END &&
+           (addr & 0x3) == 0;
+}
+
+static bool k230_dw_ssi_is_razwi(hwaddr addr)
+{
+    switch (addr) {
+    case A_XIP_CTRL:
+    case A_XIP_SER:
+    case A_XRXOICR:
+    case A_XIP_CNT_TIME_OUT:
+    case A_SPI_CTRLR1:
+    case A_SPITECR:
+    case A_RSVD_138:
+    case A_RSVD_13C:
+    case A_XIP_WRITE_INCR_INST:
+    case A_XIP_WRITE_WRAP_INST:
+    case A_XIP_WRITE_CTRL:
+        return true;
+    default:
+        return false;
+    }
+}
+
+static bool k230_dw_ssi_write_requires_disabled(hwaddr addr)
+{
+    switch (addr) {
+    case A_CTRLR0:
+    case A_CTRLR1:
+    case A_MWCR:
+    case A_BAUDR:
+    case A_SPI_CTRLR0:
+        return true;
+    default:
+        return false;
+    }
+}
+
+static uint64_t k230_dw_ssi_read(void *opaque, hwaddr addr, unsigned int size)
+{
+    K230DwSsiState *s = K230_DW_SSI(opaque);
+    uint32_t value = 0;
+
+    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);
+        }
+        return value;
+    }
+
+    if (k230_dw_ssi_is_razwi(addr)) {
+        return 0;
+    }
+
+    switch (addr) {
+    case A_CTRLR0:
+    case A_CTRLR1:
+    case A_SSIENR:
+    case A_MWCR:
+    case A_BAUDR:
+    case A_TXFTLR:
+    case A_RXFTLR:
+    case A_IMR:
+    case A_DMACR:
+    case A_AXIAWLEN:
+    case A_AXIARLEN:
+    case A_IDR:
+    case A_SSIC_VERSION_ID:
+    case A_RX_SAMPLE_DELAY:
+    case A_SPI_CTRLR0:
+    case A_DDR_DRIVE_EDGE:
+    case A_XIP_MODE_BITS:
+    case A_XIP_INCR_INST:
+    case A_XIP_WRAP_INST:
+    case A_SPIDR:
+    case A_SPIAR:
+    case A_AXIAR0:
+    case A_AXIAR1:
+        value = s->regs[addr / sizeof(uint32_t)];
+        break;
+    case A_SER:
+        value = s->regs[R_SER] & MAKE_64BIT_MASK(0, s->num_cs);
+        break;
+    case A_TXFLR:
+        value = fifo32_num_used(&s->tx_fifo);
+        break;
+    case A_RXFLR:
+        value = fifo32_num_used(&s->rx_fifo);
+        break;
+    case A_SR:
+        value = k230_dw_ssi_status(s);
+        break;
+    case A_ISR:
+    case A_RISR:
+        value = 0;
+        break;
+    case A_TXEICR:
+    case A_RXOICR:
+    case A_RXUICR:
+    case A_MSTICR:
+    case A_ICR:
+        value = 0;
+        break;
+    case A_AXIECR:
+    case A_DONECR:
+        value = 0;
+        break;
+    default:
+        if (addr >= K230_DW_SSI_REGS_SIZE || (addr & 0x3) != 0) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: bad read offset 0x%" HWADDR_PRIx "\n",
+                          DEVICE(s)->canonical_path, addr);
+        }
+        break;
+    }
+
+    return value;
+}
+
+static void k230_dw_ssi_write(void *opaque, hwaddr addr,
+                              uint64_t value, unsigned int size)
+{
+    K230DwSsiState *s = K230_DW_SSI(opaque);
+
+    if (k230_dw_ssi_is_dr(addr)) {
+        k230_dw_ssi_push_tx(s, value);
+        return;
+    }
+
+    if (k230_dw_ssi_is_razwi(addr)) {
+        return;
+    }
+
+    if (k230_dw_ssi_write_requires_disabled(addr) &&
+        k230_dw_ssi_enabled(s)) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: write to offset 0x%" HWADDR_PRIx
+                      " while SSI is enabled\n",
+                      DEVICE(s)->canonical_path, addr);
+        return;
+    }
+
+    switch (addr) {
+    case A_CTRLR0:
+        k230_dw_ssi_write_masked(s, R_CTRLR0, value,
+                                 K230_DW_SSI_CTRLR0_WRITABLE_MASK);
+        break;
+    case A_CTRLR1:
+        k230_dw_ssi_write_masked(s, R_CTRLR1, value,
+                                 K230_DW_SSI_CTRLR1_WRITABLE_MASK);
+        break;
+    case A_SSIENR: {
+        bool old_enabled = k230_dw_ssi_enabled(s);
+        bool new_enabled = value & R_SSIENR_SSIC_EN_MASK;
+
+        if (old_enabled == new_enabled) {
+            return;
+        }
+
+        s->regs[R_SSIENR] = FIELD_DP32(0, SSIENR, SSIC_EN, new_enabled);
+        if (!new_enabled) {
+            k230_dw_ssi_abort_transfer(s);
+            return;
+        }
+
+        k230_dw_ssi_update_cs(s);
+        break;
+    }
+    case A_MWCR:
+        k230_dw_ssi_write_masked(s, R_MWCR, value,
+                                 K230_DW_SSI_MWCR_WRITABLE_MASK);
+        break;
+    case A_SER:
+        s->regs[R_SER] = value & MAKE_64BIT_MASK(0, s->num_cs);
+        break;
+    case A_BAUDR:
+        k230_dw_ssi_write_masked(s, R_BAUDR, value,
+                                 K230_DW_SSI_BAUDR_WRITABLE_MASK);
+        break;
+    case A_TXFTLR:
+        k230_dw_ssi_write_masked(s, R_TXFTLR, value,
+                                 K230_DW_SSI_TXFTLR_WRITABLE_MASK);
+        break;
+    case A_RXFTLR:
+        k230_dw_ssi_write_masked(s, R_RXFTLR, value,
+                                 K230_DW_SSI_RXFTLR_WRITABLE_MASK);
+        break;
+    case A_TXFLR:
+    case A_RXFLR:
+    case A_SR:
+        break;
+    case A_IMR:
+        k230_dw_ssi_write_masked(s, R_IMR, value,
+                                 K230_DW_SSI_IMR_WRITABLE_MASK);
+        break;
+    case A_ISR:
+    case A_RISR:
+        break;
+    case A_TXEICR:
+    case A_RXOICR:
+    case A_RXUICR:
+    case A_MSTICR:
+    case A_ICR:
+        break;
+    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);
+        }
+        break;
+    case A_AXIAWLEN:
+        k230_dw_ssi_write_masked(s, R_AXIAWLEN, value,
+                                 K230_DW_SSI_AXIAWLEN_WRITABLE_MASK);
+        break;
+    case A_AXIARLEN:
+        k230_dw_ssi_write_masked(s, R_AXIARLEN, value,
+                                 K230_DW_SSI_AXIARLEN_WRITABLE_MASK);
+        break;
+    case A_IDR:
+    case A_SSIC_VERSION_ID:
+        break;
+    case A_RX_SAMPLE_DELAY:
+        k230_dw_ssi_write_masked(
+            s, R_RX_SAMPLE_DELAY, value,
+            K230_DW_SSI_RX_SAMPLE_DELAY_WRITABLE_MASK);
+        break;
+    case A_SPI_CTRLR0:
+        k230_dw_ssi_write_masked(
+            s, R_SPI_CTRLR0, value,
+            K230_DW_SSI_SPI_CTRLR0_WRITABLE_MASK);
+        break;
+    case A_DDR_DRIVE_EDGE:
+        k230_dw_ssi_write_masked(
+            s, R_DDR_DRIVE_EDGE, value,
+            K230_DW_SSI_DDR_DRIVE_EDGE_WRITABLE_MASK);
+        break;
+    case A_XIP_MODE_BITS:
+        k230_dw_ssi_write_masked(
+            s, R_XIP_MODE_BITS, value,
+            K230_DW_SSI_XIP_MODE_BITS_WRITABLE_MASK);
+        break;
+    case A_XIP_INCR_INST:
+        k230_dw_ssi_write_masked(
+            s, R_XIP_INCR_INST, value,
+            K230_DW_SSI_XIP_INCR_INST_WRITABLE_MASK);
+        break;
+    case A_XIP_WRAP_INST:
+        k230_dw_ssi_write_masked(
+            s, R_XIP_WRAP_INST, value,
+            K230_DW_SSI_XIP_WRAP_INST_WRITABLE_MASK);
+        break;
+    case A_SPIDR:
+        k230_dw_ssi_write_masked(s, R_SPIDR, value,
+                                 K230_DW_SSI_SPIDR_WRITABLE_MASK);
+        break;
+    case A_SPIAR:
+        k230_dw_ssi_write_masked(s, R_SPIAR, value,
+                                 K230_DW_SSI_SPIAR_WRITABLE_MASK);
+        break;
+    case A_AXIAR0:
+        k230_dw_ssi_write_masked(s, R_AXIAR0, value,
+                                 K230_DW_SSI_AXIAR0_WRITABLE_MASK);
+        break;
+    case A_AXIAR1:
+        k230_dw_ssi_write_masked(s, R_AXIAR1, value,
+                                 K230_DW_SSI_AXIAR1_WRITABLE_MASK);
+        break;
+    case A_AXIECR:
+    case A_DONECR:
+        break;
+    default:
+        if (addr >= K230_DW_SSI_REGS_SIZE || (addr & 0x3) != 0) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: bad write offset 0x%" HWADDR_PRIx "\n",
+                          DEVICE(s)->canonical_path, addr);
+        }
+        break;
+    }
+}
+
+static const MemoryRegionOps k230_dw_ssi_ops = {
+    .read = k230_dw_ssi_read,
+    .write = k230_dw_ssi_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .impl = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+    .valid = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+};
+
+static void k230_dw_ssi_enter_reset(Object *obj, ResetType type)
+{
+    K230DwSsiState *s = K230_DW_SSI(obj);
+
+    memset(s->regs, 0, sizeof(s->regs));
+    fifo32_reset(&s->tx_fifo);
+    fifo32_reset(&s->rx_fifo);
+
+    s->regs[R_CTRLR0] = K230_DW_SSI_CTRLR0_RESET;
+    s->regs[R_SR] = K230_DW_SSI_SR_RESET;
+    s->regs[R_IMR] = K230_DW_SSI_IMR_RESET;
+    s->regs[R_AXIAWLEN] = K230_DW_SSI_AXIAWLEN_RESET;
+    s->regs[R_AXIARLEN] = K230_DW_SSI_AXIARLEN_RESET;
+    s->regs[R_IDR] = K230_DW_SSI_IDR_RESET;
+    s->regs[R_SSIC_VERSION_ID] = K230_DW_SSI_VERSION;
+    s->regs[R_SPI_CTRLR0] = s->max_lines == 8 ?
+        K230_DW_SSI_SPI_CTRLR0_FMC_RESET :
+        K230_DW_SSI_SPI_CTRLR0_SPI_RESET;
+}
+
+static void k230_dw_ssi_hold_reset(Object *obj, ResetType type)
+{
+    K230DwSsiState *s = K230_DW_SSI(obj);
+    s->active_cs = -1;
+
+    if (s->cs_lines) {
+        for (int i = 0; i < s->num_cs; i++) {
+            qemu_irq_raise(s->cs_lines[i]);
+        }
+    }
+}
+
+static const VMStateDescription vmstate_k230_dw_ssi = {
+    .name = TYPE_K230_DW_SSI,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32_ARRAY(regs, K230DwSsiState, K230_DW_SSI_NUM_REGS),
+        VMSTATE_FIFO32(tx_fifo, K230DwSsiState),
+        VMSTATE_FIFO32(rx_fifo, K230DwSsiState),
+        VMSTATE_INT32(active_cs, K230DwSsiState),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void k230_dw_ssi_init(Object *obj)
+{
+    K230DwSsiState *s = K230_DW_SSI(obj);
+    DeviceState *dev = DEVICE(obj);
+    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+
+    s->spi = ssi_create_bus(dev, "spi");
+
+    memory_region_init_io(&s->mmio, obj, &k230_dw_ssi_ops, s,
+                          TYPE_K230_DW_SSI, K230_DW_SSI_MMIO_SIZE);
+    sysbus_init_mmio(sbd, &s->mmio);
+
+    fifo32_create(&s->tx_fifo, K230_DW_SSI_FIFO_CAPACITY);
+    fifo32_create(&s->rx_fifo, K230_DW_SSI_FIFO_CAPACITY);
+    s->active_cs = -1;
+}
+
+static void k230_dw_ssi_realize(DeviceState *dev, Error **errp)
+{
+    K230DwSsiState *s = K230_DW_SSI(dev);
+
+    if (s->num_cs == 0 || s->num_cs > 8) {
+        error_setg(errp, "%s: num-cs must be in range 1..8",
+                   dev->canonical_path);
+        return;
+    }
+
+    if (s->max_lines != 1 && s->max_lines != 4 && s->max_lines != 8) {
+        error_setg(errp, "%s: max-lines must be 1, 4, or 8",
+                   dev->canonical_path);
+        return;
+    }
+
+    s->cs_lines = g_new0(qemu_irq, s->num_cs);
+    qdev_init_gpio_out_named(dev, s->cs_lines, "cs", s->num_cs);
+}
+
+static void k230_dw_ssi_finalize(Object *obj)
+{
+    K230DwSsiState *s = K230_DW_SSI(obj);
+
+    fifo32_destroy(&s->tx_fifo);
+    fifo32_destroy(&s->rx_fifo);
+    g_free(s->cs_lines);
+}
+
+static const Property k230_dw_ssi_properties[] = {
+    DEFINE_PROP_UINT32("num-cs", K230DwSsiState, num_cs, 1),
+    DEFINE_PROP_UINT32("max-lines", K230DwSsiState, max_lines, 1),
+};
+
+static void k230_dw_ssi_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->realize = k230_dw_ssi_realize;
+    dc->vmsd = &vmstate_k230_dw_ssi;
+    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;
+}
+
+static const TypeInfo k230_dw_ssi_info = {
+    .name = TYPE_K230_DW_SSI,
+    .parent = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(K230DwSsiState),
+    .instance_init = k230_dw_ssi_init,
+    .instance_finalize = k230_dw_ssi_finalize,
+    .class_init = k230_dw_ssi_class_init,
+};
+
+static void k230_dw_ssi_register_types(void)
+{
+    type_register_static(&k230_dw_ssi_info);
+}
+
+type_init(k230_dw_ssi_register_types)
diff --git a/hw/ssi/meson.build b/hw/ssi/meson.build
index 6afb1ea200..1e9c1d72c4 100644
--- a/hw/ssi/meson.build
+++ b/hw/ssi/meson.build
@@ -1,6 +1,7 @@
 system_ss.add(when: 'CONFIG_ALLWINNER_A10_SPI', if_true: files('allwinner-a10-spi.c'))
 system_ss.add(when: 'CONFIG_ASPEED_SOC', if_true: files('aspeed_smc.c'))
 system_ss.add(when: 'CONFIG_MSF2', if_true: files('mss-spi.c'))
+system_ss.add(when: 'CONFIG_K230_DW_SSI', if_true: files('k230_dw_ssi.c'))
 system_ss.add(when: 'CONFIG_NPCM7XX', if_true: files('npcm7xx_fiu.c', 'npcm_pspi.c'))
 system_ss.add(when: 'CONFIG_PL022', if_true: files('pl022.c'))
 system_ss.add(when: 'CONFIG_SIFIVE_SPI', if_true: files('sifive_spi.c'))
diff --git a/include/hw/ssi/k230_dw_ssi.h b/include/hw/ssi/k230_dw_ssi.h
new file mode 100644
index 0000000000..953f6cc0e5
--- /dev/null
+++ b/include/hw/ssi/k230_dw_ssi.h
@@ -0,0 +1,50 @@
+/*
+ * Kendryte K230 DesignWare SSI
+ *
+ * Copyright (c) 2026 Kangjie Huang <flamboyant.h.01@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Emulates the DesignWare SSI controllers documented by the K230
+ * Technical Reference Manual, including standard SPI, Dual/Quad SDR,
+ * internal DMA, and the XIP read window.
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18):
+ * https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
+ *
+ * For more information, see <https://www.kendryte.com/en/proDetail/230>
+ */
+
+#ifndef HW_SSI_K230_DW_SSI_H
+#define HW_SSI_K230_DW_SSI_H
+
+#include "hw/core/sysbus.h"
+#include "hw/ssi/ssi.h"
+#include "qemu/fifo32.h"
+#include "qom/object.h"
+
+#define TYPE_K230_DW_SSI "riscv.k230.dw-ssi"
+OBJECT_DECLARE_SIMPLE_TYPE(K230DwSsiState, K230_DW_SSI)
+
+#define K230_DW_SSI_MMIO_SIZE 0x1000
+#define K230_DW_SSI_REGS_SIZE 0x14c
+#define K230_DW_SSI_NUM_REGS \
+    (K230_DW_SSI_REGS_SIZE / sizeof(uint32_t))
+
+struct K230DwSsiState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion mmio;
+    SSIBus *spi;
+    qemu_irq *cs_lines;
+
+    Fifo32 tx_fifo;
+    Fifo32 rx_fifo;
+    uint32_t regs[K230_DW_SSI_NUM_REGS];
+
+    uint32_t num_cs;
+    uint32_t max_lines;
+    int active_cs;
+};
+
+#endif /* HW_SSI_K230_DW_SSI_H */
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 02/11] hw/riscv/k230: Instantiate K230 SSI controllers
  2026-07-26 12:28 ` Kangjie Huang
  (?)
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Instantiate the three SSI controller profiles used by K230 and map their
MMIO regions at the documented addresses.

Add the K230 SSI qtest target. Cover the three instance profiles, reset
state, chip-select masks, representative register masks, and system
reset behaviour.

Signed-off-by: Kangjie Huang <flamboyant.h.01@gmail.com>
---
 docs/system/riscv/k230.rst     |   1 +
 hw/riscv/Kconfig               |   1 +
 hw/riscv/k230.c                |  35 +++++--
 include/hw/riscv/k230.h        |   2 +
 tests/qtest/k230-dw-ssi-test.c | 178 +++++++++++++++++++++++++++++++++
 tests/qtest/meson.build        |   4 +-
 6 files changed, 211 insertions(+), 10 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..0358709d2c 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 and QSPI
 
 Boot options
 ------------
diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig
index de37c08cae..69059ae044 100644
--- a/hw/riscv/Kconfig
+++ b/hw/riscv/Kconfig
@@ -162,3 +162,4 @@ config K230
     select SERIAL_MM
     select UNIMP
     select K230_WDT
+    select K230_DW_SSI
diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
index 656f28190c..acd5445b31 100644
--- a/hw/riscv/k230.c
+++ b/hw/riscv/k230.c
@@ -110,6 +110,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_K230_DW_SSI);
+    object_initialize_child(obj, "k230-qspi1", &s->dw_ssi[1],
+                            TYPE_K230_DW_SSI);
+    object_initialize_child(obj, "k230-spi-opi", &s->dw_ssi[2],
+                            TYPE_K230_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 +204,19 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
         }
     }
 
+    qdev_prop_set_uint32(DEVICE(&s->dw_ssi[0]), "num-cs", 5);
+    qdev_prop_set_uint32(DEVICE(&s->dw_ssi[1]), "num-cs", 5);
+    qdev_prop_set_uint32(DEVICE(&s->dw_ssi[2]), "num-cs", 1);
+    qdev_prop_set_uint32(DEVICE(&s->dw_ssi[0]), "max-lines", 4);
+    qdev_prop_set_uint32(DEVICE(&s->dw_ssi[1]), "max-lines", 4);
+    qdev_prop_set_uint32(DEVICE(&s->dw_ssi[2]), "max-lines", 8);
+
+    for (int i = 0; i < ARRAY_SIZE(s->dw_ssi); 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 +225,13 @@ 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);
+
     /* unimplemented devices */
     create_unimplemented_device("kpu.l2-cache",
                                 memmap[K230_DEV_KPU_L2_CACHE].base,
@@ -349,15 +375,6 @@ 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);
 
diff --git a/include/hw/riscv/k230.h b/include/hw/riscv/k230.h
index 592e1c26bf..4a79b7ed45 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/k230_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];
+    K230DwSsiState 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..dcd6cb1d6e
--- /dev/null
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -0,0 +1,178 @@
+/*
+ * QTest for the Kendryte K230 DesignWare SSI
+ *
+ * Copyright (c) 2026 Kangjie Huang <flamboyant.h.01@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "libqtest.h"
+#include "qemu/bitops.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_DMACR           0x04c
+#define K230_SSI_IDR             0x058
+#define K230_SSI_VERSION_ID      0x05c
+#define K230_SSI_SPI_CTRLR0      0x0f4
+#define K230_SSI_XIP_MODE_BITS   0x0fc
+#define K230_SSI_XIP_INCR_INST   0x100
+#define K230_SSI_SPIDR           0x120
+#define K230_SSI_SPIAR           0x124
+#define K230_SSI_AXIAR0          0x128
+#define K230_SSI_AXIAR1          0x12c
+#define K230_SSI_AXIECR          0x130
+#define K230_SSI_DONECR          0x134
+
+#define K230_SSI_CTRLR0_RESET           0x00004007U
+#define K230_SSI_SPI_CTRLR0_SPI_RESET   0x04000200U
+#define K230_SSI_SPI_CTRLR0_FMC_RESET   0x28000200U
+#define K230_SSI_IMR_RESET              0x0000003fU
+#define K230_SSI_IDR_RESET              0xa1b2c3d5U
+#define K230_SSI_VERSION_RESET          0x3130332aU
+
+#define K230_SSI_CTRLR0_WRITABLE_MASK   0x01cf7f1fU
+#define K230_SSI_BAUDR_WRITABLE_MASK    0x0000fffeU
+
+#define K230_SSI_CTRLR0_DFS_MASK        0x1fU
+#define K230_SSI_CTRLR0_TMOD_SHIFT      10
+
+#define K230_SSI_TMOD_TR                0
+#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_AXIE               BIT(8)
+#define K230_SSI_INT_DONE               BIT(11)
+
+typedef struct K230SsiInstance {
+    uint64_t base;
+    uint32_t num_cs;
+    uint32_t spi_ctrlr0_reset;
+} K230SsiInstance;
+
+static const K230SsiInstance k230_ssi_instances[3] = {
+    {
+        .base = K230_SPI0_BASE,
+        .num_cs = 1,
+        .spi_ctrlr0_reset = K230_SSI_SPI_CTRLR0_FMC_RESET,
+    }, {
+        .base = K230_SPI1_BASE,
+        .num_cs = 5,
+        .spi_ctrlr0_reset = K230_SSI_SPI_CTRLR0_SPI_RESET,
+    }, {
+        .base = K230_SPI2_BASE,
+        .num_cs = 5,
+        .spi_ctrlr0_reset = K230_SSI_SPI_CTRLR0_SPI_RESET,
+    },
+};
+
+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 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),
+                        ==, inst->spi_ctrlr0_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);
+    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);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_IMR),
+                    ==, K230_SSI_IMR_RESET);
+    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);
+    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



^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 03/11] hw/ssi: Implement K230 SSI FIFO and standard PIO transfers
  2026-07-26 12:28 ` Kangjie Huang
                   ` (2 preceding siblings ...)
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Implement FIFO-backed standard SPI transfers for all four TMOD modes.

Writes to DR while the TX FIFO is full are dropped, and frames wider
than the DFS field are truncated on read, per the K230 TRM. Disabling
the controller stops the serial engine but leaves queued frames in the
FIFOs.

Extend the qtest with an 8-bit loopback transfer, receive-only NDF
handling, FIFO status checks, and FIFO cleanup on controller disable.

Signed-off-by: Kangjie Huang <flamboyant.h.01@gmail.com>
---
 hw/ssi/k230_dw_ssi.c           | 160 ++++++++++++++++++++++++++++++++-
 include/hw/ssi/k230_dw_ssi.h   |  11 +++
 tests/qtest/k230-dw-ssi-test.c |  77 ++++++++++++++++
 3 files changed, 245 insertions(+), 3 deletions(-)

diff --git a/hw/ssi/k230_dw_ssi.c b/hw/ssi/k230_dw_ssi.c
index d0dcad4312..d7315f28ef 100644
--- a/hw/ssi/k230_dw_ssi.c
+++ b/hw/ssi/k230_dw_ssi.c
@@ -37,6 +37,7 @@
 #define K230_DW_SSI_AXIAWLEN_RESET          0x00000700
 #define K230_DW_SSI_AXIARLEN_RESET          0x00000700
 #define K230_DW_SSI_VERSION                 0x3130332a
+#define K230_DW_SSI_PIO_TX_BATCH            64
 
 REG32(CTRLR0, 0x000)
     FIELD(CTRLR0, DFS, 0, 5)
@@ -330,6 +331,8 @@ static void k230_dw_ssi_abort_transfer(K230DwSsiState *s)
     k230_dw_ssi_deselect(s);
     fifo32_reset(&s->tx_fifo);
     fifo32_reset(&s->rx_fifo);
+    s->phase = K230_DW_SSI_PHASE_IDLE;
+    s->remaining_frames = 0;
 }
 
 static uint32_t k230_dw_ssi_status(K230DwSsiState *s)
@@ -338,6 +341,7 @@ static uint32_t k230_dw_ssi_status(K230DwSsiState *s)
     uint32_t rx_used = fifo32_num_used(&s->rx_fifo);
     uint32_t sr = 0;
 
+    sr = FIELD_DP32(sr, SR, BUSY, s->phase != K230_DW_SSI_PHASE_IDLE);
     sr = FIELD_DP32(sr, SR, TFNF, tx_used < K230_DW_SSI_FIFO_CAPACITY);
     sr = FIELD_DP32(sr, SR, TFE, tx_used == 0);
     sr = FIELD_DP32(sr, SR, RFNE, rx_used != 0);
@@ -351,7 +355,7 @@ static void k230_dw_ssi_run_transfer(K230DwSsiState *s);
 
 static void k230_dw_ssi_push_tx(K230DwSsiState *s, uint32_t tx)
 {
-    if (!k230_dw_ssi_enabled(s) || s->active_cs < 0) {
+    if (!k230_dw_ssi_enabled(s)) {
         return;
     }
 
@@ -360,12 +364,139 @@ static void k230_dw_ssi_push_tx(K230DwSsiState *s, uint32_t tx)
     }
 
     fifo32_push(&s->tx_fifo, tx & k230_dw_ssi_frame_masked(s));
-    k230_dw_ssi_run_transfer(s);
+
+    if (s->phase != K230_DW_SSI_PHASE_STANDARD_TX_ONLY) {
+        k230_dw_ssi_run_transfer(s);
+    }
 }
 
+static uint32_t k230_dw_ssi_send_frame(K230DwSsiState *s,
+                                        uint32_t tx)
+{
+    uint32_t mask = k230_dw_ssi_frame_masked(s);
+    uint32_t rx;
+
+    tx &= mask;
+
+    if (FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, SRL)) {
+        rx = tx;
+    } else {
+        rx = ssi_transfer(s->spi, tx);
+    }
+
+    return rx & mask;
+}
 
 static void k230_dw_ssi_run_transfer(K230DwSsiState *s)
 {
+    uint32_t spi_frf;
+    uint32_t tmod;
+
+    if (!k230_dw_ssi_enabled(s) || s->active_cs < 0) {
+        return;
+    }
+
+    spi_frf = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, SPI_FRF);
+    if (spi_frf != 0) {
+        return;
+    }
+
+    tmod = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, TMOD);
+
+    switch (tmod) {
+    case 0: /* TX_AND_RX */
+        while (!fifo32_is_empty(&s->tx_fifo)) {
+            uint32_t tx = fifo32_pop(&s->tx_fifo);
+            uint32_t rx = k230_dw_ssi_send_frame(s, tx);
+            if (!fifo32_is_full(&s->rx_fifo)) {
+                fifo32_push(&s->rx_fifo, rx);
+            }
+        }
+        break;
+    case 1: { /* TX_ONLY */
+        unsigned int frames = 0;
+
+        if (fifo32_is_empty(&s->tx_fifo)) {
+            s->phase = K230_DW_SSI_PHASE_IDLE;
+            break;
+        }
+
+        s->phase = K230_DW_SSI_PHASE_STANDARD_TX_ONLY;
+        while (!fifo32_is_empty(&s->tx_fifo) &&
+               frames < K230_DW_SSI_PIO_TX_BATCH) {
+            uint32_t tx = fifo32_pop(&s->tx_fifo);
+
+            k230_dw_ssi_send_frame(s, tx);
+            frames++;
+        }
+        if (fifo32_is_empty(&s->tx_fifo)) {
+            s->phase = K230_DW_SSI_PHASE_IDLE;
+        }
+        break;
+    }
+    case 2: /* RX_ONLY */
+        switch (s->phase) {
+        case K230_DW_SSI_PHASE_IDLE:
+            if (fifo32_is_empty(&s->tx_fifo)) {
+                break;
+            }
+            fifo32_pop(&s->tx_fifo);
+            s->phase = K230_DW_SSI_PHASE_RX_ONLY;
+            s->remaining_frames = FIELD_EX32(s->regs[R_CTRLR1], CTRLR1,
+                                              NDF) + 1;
+            /* Fall through. */
+        case K230_DW_SSI_PHASE_RX_ONLY:
+            while (!fifo32_is_full(&s->rx_fifo) &&
+                   s->remaining_frames > 0) {
+                uint32_t rx = k230_dw_ssi_send_frame(s, 0x00);
+
+                fifo32_push(&s->rx_fifo, rx);
+                s->remaining_frames--;
+            }
+            if (s->remaining_frames == 0) {
+                s->phase = K230_DW_SSI_PHASE_IDLE;
+            }
+            break;
+        }
+        break;
+    case 3: /* EEPROM_READ */
+        switch (s->phase) {
+        case K230_DW_SSI_PHASE_IDLE:
+            if (fifo32_is_empty(&s->tx_fifo)) {
+                break;
+            }
+            s->phase = K230_DW_SSI_PHASE_EEPROM_COMMAND;
+            /* Fall through. */
+        case K230_DW_SSI_PHASE_EEPROM_COMMAND:
+            if (fifo32_is_empty(&s->tx_fifo)) {
+                break;
+            }
+            while (!fifo32_is_empty(&s->tx_fifo)) {
+                uint32_t tx = fifo32_pop(&s->tx_fifo);
+
+                k230_dw_ssi_send_frame(s, tx);
+            }
+            s->phase = K230_DW_SSI_PHASE_EEPROM_DATA;
+            s->remaining_frames = FIELD_EX32(s->regs[R_CTRLR1], CTRLR1,
+                                              NDF) + 1;
+            /* Fall through. */
+        case K230_DW_SSI_PHASE_EEPROM_DATA:
+            while (!fifo32_is_full(&s->rx_fifo) &&
+                   s->remaining_frames > 0) {
+                uint32_t rx = k230_dw_ssi_send_frame(s, 0x00);
+
+                fifo32_push(&s->rx_fifo, rx);
+                s->remaining_frames--;
+            }
+            if (s->remaining_frames == 0) {
+                s->phase = K230_DW_SSI_PHASE_IDLE;
+            }
+            break;
+        }
+        break;
+    default:
+        g_assert_not_reached();
+    }
 }
 
 
@@ -419,6 +550,8 @@ static uint64_t k230_dw_ssi_read(void *opaque, hwaddr addr, unsigned int size)
         if (!fifo32_is_empty(&s->rx_fifo)) {
             value = fifo32_pop(&s->rx_fifo) & k230_dw_ssi_frame_masked(s);
         }
+
+        k230_dw_ssi_run_transfer(s);
         return value;
     }
 
@@ -457,12 +590,18 @@ static uint64_t k230_dw_ssi_read(void *opaque, hwaddr addr, unsigned int size)
         break;
     case A_TXFLR:
         value = fifo32_num_used(&s->tx_fifo);
+        if (s->phase == K230_DW_SSI_PHASE_STANDARD_TX_ONLY) {
+            k230_dw_ssi_run_transfer(s);
+        }
         break;
     case A_RXFLR:
         value = fifo32_num_used(&s->rx_fifo);
         break;
     case A_SR:
         value = k230_dw_ssi_status(s);
+        if (s->phase == K230_DW_SSI_PHASE_STANDARD_TX_ONLY) {
+            k230_dw_ssi_run_transfer(s);
+        }
         break;
     case A_ISR:
     case A_RISR:
@@ -538,15 +677,26 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
         }
 
         k230_dw_ssi_update_cs(s);
+        k230_dw_ssi_run_transfer(s);
         break;
     }
     case A_MWCR:
         k230_dw_ssi_write_masked(s, R_MWCR, value,
                                  K230_DW_SSI_MWCR_WRITABLE_MASK);
         break;
-    case A_SER:
+    case A_SER: {
+        uint32_t old_ser = s->regs[R_SER];
+
         s->regs[R_SER] = value & MAKE_64BIT_MASK(0, s->num_cs);
+        if (old_ser && !s->regs[R_SER]) {
+            k230_dw_ssi_abort_transfer(s);
+            break;
+        }
+
+        k230_dw_ssi_update_cs(s);
+        k230_dw_ssi_run_transfer(s);
         break;
+    }
     case A_BAUDR:
         k230_dw_ssi_write_masked(s, R_BAUDR, value,
                                  K230_DW_SSI_BAUDR_WRITABLE_MASK);
@@ -679,6 +829,8 @@ static void k230_dw_ssi_enter_reset(Object *obj, ResetType type)
     memset(s->regs, 0, sizeof(s->regs));
     fifo32_reset(&s->tx_fifo);
     fifo32_reset(&s->rx_fifo);
+    s->phase = K230_DW_SSI_PHASE_IDLE;
+    s->remaining_frames = 0;
 
     s->regs[R_CTRLR0] = K230_DW_SSI_CTRLR0_RESET;
     s->regs[R_SR] = K230_DW_SSI_SR_RESET;
@@ -710,6 +862,8 @@ static const VMStateDescription vmstate_k230_dw_ssi = {
         VMSTATE_UINT32_ARRAY(regs, K230DwSsiState, K230_DW_SSI_NUM_REGS),
         VMSTATE_FIFO32(tx_fifo, K230DwSsiState),
         VMSTATE_FIFO32(rx_fifo, K230DwSsiState),
+        VMSTATE_UINT32(phase, K230DwSsiState),
+        VMSTATE_UINT32(remaining_frames, K230DwSsiState),
         VMSTATE_INT32(active_cs, K230DwSsiState),
         VMSTATE_END_OF_LIST()
     },
diff --git a/include/hw/ssi/k230_dw_ssi.h b/include/hw/ssi/k230_dw_ssi.h
index 953f6cc0e5..993cb19745 100644
--- a/include/hw/ssi/k230_dw_ssi.h
+++ b/include/hw/ssi/k230_dw_ssi.h
@@ -31,6 +31,14 @@ OBJECT_DECLARE_SIMPLE_TYPE(K230DwSsiState, K230_DW_SSI)
 #define K230_DW_SSI_NUM_REGS \
     (K230_DW_SSI_REGS_SIZE / sizeof(uint32_t))
 
+typedef enum K230DwSsiPhase {
+    K230_DW_SSI_PHASE_IDLE,
+    K230_DW_SSI_PHASE_STANDARD_TX_ONLY,
+    K230_DW_SSI_PHASE_RX_ONLY,
+    K230_DW_SSI_PHASE_EEPROM_COMMAND,
+    K230_DW_SSI_PHASE_EEPROM_DATA,
+} K230DwSsiPhase;
+
 struct K230DwSsiState {
     SysBusDevice parent_obj;
 
@@ -42,6 +50,9 @@ struct K230DwSsiState {
     Fifo32 rx_fifo;
     uint32_t regs[K230_DW_SSI_NUM_REGS];
 
+    uint32_t phase;
+    uint32_t remaining_frames;
+
     uint32_t num_cs;
     uint32_t max_lines;
     int active_cs;
diff --git a/tests/qtest/k230-dw-ssi-test.c b/tests/qtest/k230-dw-ssi-test.c
index dcd6cb1d6e..a62b1a7ff6 100644
--- a/tests/qtest/k230-dw-ssi-test.c
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -28,6 +28,7 @@
 #define K230_SSI_DMACR           0x04c
 #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_XIP_MODE_BITS   0x0fc
 #define K230_SSI_XIP_INCR_INST   0x100
@@ -50,8 +51,11 @@
 
 #define K230_SSI_CTRLR0_DFS_MASK        0x1fU
 #define K230_SSI_CTRLR0_TMOD_SHIFT      10
+#define K230_SSI_CTRLR0_SRL             BIT(13)
 
 #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)
@@ -62,6 +66,8 @@
 #define K230_SSI_INT_AXIE               BIT(8)
 #define K230_SSI_INT_DONE               BIT(11)
 
+#define K230_SSI_FIFO_DEPTH             256
+
 typedef struct K230SsiInstance {
     uint64_t base;
     uint32_t num_cs;
@@ -130,6 +136,45 @@ static void k230_ssi_enable_cs(QTestState *qts, uint64_t base, uint32_t 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();
@@ -169,10 +214,42 @@ static void test_register_contract(void)
     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);
+}
+
 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);
     return g_test_run();
 }
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 04/11] hw/ssi: Add K230 SSI interrupt controller
  2026-07-26 12:28 ` Kangjie Huang
                   ` (3 preceding siblings ...)
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

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 <flamboyant.h.01@gmail.com>
---
 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



^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 05/11] hw/riscv: Route K230 SSI IRQs to the PLIC
  2026-07-26 12:28 ` Kangjie Huang
                   ` (4 preceding siblings ...)
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Connect the nine GPIO outputs from each K230 SSI controller to their PLIC
sources.

Check the TXE PLIC route for all three instances and verify RXU routing
and instance isolation for spi1 in qtest.

Signed-off-by: Kangjie Huang <flamboyant.h.01@gmail.com>
---
 hw/riscv/k230.c                | 36 +++++++++++++++++++++++++
 include/hw/riscv/k230.h        |  3 +++
 tests/qtest/k230-dw-ssi-test.c | 48 ++++++++++++++++++++++++++++++++++
 3 files changed, 87 insertions(+)

diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
index acd5445b31..1863d5ecbb 100644
--- a/hw/riscv/k230.c
+++ b/hw/riscv/k230.c
@@ -102,6 +102,21 @@ static const MemMapEntry memmap[] = {
     [K230_DEV_CLINT] =        { 0xF04000000, 0x00400000 },
 };
 
+typedef struct K230SsiRoute {
+    unsigned int ssi_index;
+    unsigned int irq_base;
+} K230SsiRoute;
+
+/*
+ * SDK numbering follows the address map: spi0 is the SPI-OPI instance,
+ * while spi1 and spi2 are QSPI0 and QSPI1.
+ */
+static const K230SsiRoute k230_ssi_routes[] = {
+    { .ssi_index = 2, .irq_base = K230_SPI0_IRQ_BASE },
+    { .ssi_index = 0, .irq_base = K230_SPI1_IRQ_BASE },
+    { .ssi_index = 1, .irq_base = K230_SPI2_IRQ_BASE },
+};
+
 static void k230_soc_init(Object *obj)
 {
     K230SoCState *s = RISCV_K230_SOC(obj);
@@ -157,6 +172,25 @@ static void k230_create_uart(MemoryRegion *sys_mem, DeviceState *plic,
                    399193, serial_hd(index), DEVICE_LITTLE_ENDIAN);
 }
 
+static void k230_connect_ssi_irqs(K230SoCState *s)
+{
+    for (size_t route_idx = 0;
+         route_idx < ARRAY_SIZE(k230_ssi_routes); route_idx++) {
+        const K230SsiRoute *route = &k230_ssi_routes[route_idx];
+        SysBusDevice *ssi;
+
+        g_assert(route->ssi_index < ARRAY_SIZE(s->dw_ssi));
+        g_assert(route->irq_base + K230_DW_SSI_IRQ_COUNT <=
+                 K230_PLIC_NUM_SOURCES);
+
+        ssi = SYS_BUS_DEVICE(&s->dw_ssi[route->ssi_index]);
+        for (unsigned int i = 0; i < K230_DW_SSI_IRQ_COUNT; i++) {
+            sysbus_connect_irq(ssi, i,
+                qdev_get_gpio_in(DEVICE(s->c908_plic), route->irq_base + i));
+        }
+    }
+}
+
 static void k230_soc_realize(DeviceState *dev, Error **errp)
 {
     K230SoCState *s = RISCV_K230_SOC(dev);
@@ -217,6 +251,8 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
         }
     }
 
+    k230_connect_ssi_irqs(s);
+
     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));
diff --git a/include/hw/riscv/k230.h b/include/hw/riscv/k230.h
index 4a79b7ed45..cfe30590cc 100644
--- a/include/hw/riscv/k230.h
+++ b/include/hw/riscv/k230.h
@@ -131,6 +131,9 @@ enum {
     K230_UART4_IRQ  = 20,
     K230_WDT0_IRQ   = 107,
     K230_WDT1_IRQ   = 108,
+    K230_SPI0_IRQ_BASE = 146,
+    K230_SPI1_IRQ_BASE = 155,
+    K230_SPI2_IRQ_BASE = 164,
 };
 
 #define K230_UART_COUNT 5
diff --git a/tests/qtest/k230-dw-ssi-test.c b/tests/qtest/k230-dw-ssi-test.c
index 0a3d03d6f3..e6c0e2ee8c 100644
--- a/tests/qtest/k230-dw-ssi-test.c
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -13,6 +13,8 @@
 #define K230_SPI0_BASE          0x91584000ULL
 #define K230_SPI1_BASE          0x91582000ULL
 #define K230_SPI2_BASE          0x91583000ULL
+#define K230_PLIC_BASE          0xf00000000ULL
+#define K230_PLIC_PENDING_BASE  0x1000
 #define K230_SSI_CTRLR0          0x000
 #define K230_SSI_CTRLR1          0x004
 #define K230_SSI_SSIENR          0x008
@@ -69,12 +71,18 @@
 #define K230_SSI_INT_AXIE               BIT(8)
 #define K230_SSI_INT_DONE               BIT(11)
 
+#define K230_SSI_IRQ_TXE                0
+#define K230_SSI_IRQ_RXU                5
+#define K230_SSI_IRQ_DONE               7
+#define K230_SSI_IRQ_AXIE               8
+
 #define K230_SSI_FIFO_DEPTH             256
 
 typedef struct K230SsiInstance {
     uint64_t base;
     uint32_t num_cs;
     uint32_t spi_ctrlr0_reset;
+    uint32_t first_irq;
 } K230SsiInstance;
 
 static const K230SsiInstance k230_ssi_instances[3] = {
@@ -82,14 +90,17 @@ static const K230SsiInstance k230_ssi_instances[3] = {
         .base = K230_SPI0_BASE,
         .num_cs = 1,
         .spi_ctrlr0_reset = K230_SSI_SPI_CTRLR0_FMC_RESET,
+        .first_irq = 146,
     }, {
         .base = K230_SPI1_BASE,
         .num_cs = 5,
         .spi_ctrlr0_reset = K230_SSI_SPI_CTRLR0_SPI_RESET,
+        .first_irq = 155,
     }, {
         .base = K230_SPI2_BASE,
         .num_cs = 5,
         .spi_ctrlr0_reset = K230_SSI_SPI_CTRLR0_SPI_RESET,
+        .first_irq = 164,
     },
 };
 
@@ -178,6 +189,14 @@ static void configure_loopback(QTestState *qts, uint32_t tmod,
                     ctrlr0 | K230_SSI_CTRLR0_SRL);
 }
 
+static bool k230_ssi_plic_pending(QTestState *qts, uint32_t irq)
+{
+    uint64_t addr = K230_PLIC_BASE + K230_PLIC_PENDING_BASE +
+                    (irq / 32) * sizeof(uint32_t);
+
+    return qtest_readl(qts, addr) & BIT(irq % 32);
+}
+
 static void test_register_contract(void)
 {
     QTestState *qts = k230_ssi_start();
@@ -279,6 +298,34 @@ static void test_interrupt_controller(void)
     qtest_quit(qts);
 }
 
+static void test_plic_routing(void)
+{
+    QTestState *qts = k230_ssi_start();
+    const K230SsiInstance *target = &k230_ssi_instances[1];
+
+    for (int i = 0; i < ARRAY_SIZE(k230_ssi_instances); i++) {
+        const K230SsiInstance *inst = &k230_ssi_instances[i];
+
+        g_assert_true(k230_ssi_plic_pending(qts,
+                                           inst->first_irq +
+                                           K230_SSI_IRQ_TXE));
+        k230_ssi_writel(qts, inst->base, K230_SSI_IMR, 0);
+    }
+
+    k230_ssi_writel(qts, target->base, K230_SSI_IMR, K230_SSI_INT_RXU);
+    (void)k230_ssi_read_frame(qts, target->base);
+    g_assert_true(k230_ssi_plic_pending(qts,
+                                       target->first_irq +
+                                       K230_SSI_IRQ_RXU));
+    for (int i = 0; i < ARRAY_SIZE(k230_ssi_instances); i++) {
+        if (&k230_ssi_instances[i] != target) {
+            g_assert_false(k230_ssi_plic_pending(
+                qts, k230_ssi_instances[i].first_irq + K230_SSI_IRQ_RXU));
+        }
+    }
+    qtest_quit(qts);
+}
+
 int main(int argc, char **argv)
 {
     g_test_init(&argc, &argv, NULL);
@@ -287,5 +334,6 @@ int main(int argc, char **argv)
     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/plic-routing", test_plic_routing);
     return g_test_run();
 }
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 06/11] hw/ssi: Implement K230 enhanced QSPI transfers
  2026-07-26 12:28 ` Kangjie Huang
                   ` (5 preceding siblings ...)
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Add controller-side Dual and Quad SDR phases for instruction, address,
mode, dummy, and data fields.

Reject unsupported Octal, DDR, RXDS, and invalid transfer configurations
without consuming FIFO data.

Use qtest to check an accepted Quad SDR configuration and representative
Octal and DDR rejection paths.

According to the K230 SDK driver, multi-line QSPI data transfers use the
controller's internal IDMA path. That SDK-facing path is implemented by
a later patch.

Signed-off-by: Kangjie Huang <flamboyant.h.01@gmail.com>
---
 hw/ssi/k230_dw_ssi.c           | 297 ++++++++++++++++++++++++++++++++-
 include/hw/ssi/k230_dw_ssi.h   |  22 +++
 tests/qtest/k230-dw-ssi-test.c |  65 ++++++++
 3 files changed, 376 insertions(+), 8 deletions(-)

diff --git a/hw/ssi/k230_dw_ssi.c b/hw/ssi/k230_dw_ssi.c
index 26ac052a63..31da3a06a3 100644
--- a/hw/ssi/k230_dw_ssi.c
+++ b/hw/ssi/k230_dw_ssi.c
@@ -40,6 +40,13 @@
 #define K230_DW_SSI_PIO_TX_BATCH            64
 #define K230_DW_SSI_IRQ_VALID_MASK          0x000009bf
 
+enum {
+    K230_DW_SSI_TMOD_TR,
+    K230_DW_SSI_TMOD_TO,
+    K230_DW_SSI_TMOD_RO,
+    K230_DW_SSI_TMOD_EEPROM_READ,
+};
+
 REG32(CTRLR0, 0x000)
     FIELD(CTRLR0, DFS, 0, 5)
     FIELD(CTRLR0, FRF, 6, 2)
@@ -324,7 +331,6 @@ 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)
 {
     return FIELD_EX32(s->regs[R_SSIENR], SSIENR, SSIC_EN);
@@ -386,6 +392,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;
+    memset(&s->enhanced, 0, sizeof(s->enhanced));
     k230_dw_ssi_update_irq(s);
 }
 
@@ -419,7 +426,7 @@ static void k230_dw_ssi_push_tx(K230DwSsiState *s, uint32_t tx)
         return;
     }
 
-    fifo32_push(&s->tx_fifo, tx & k230_dw_ssi_frame_masked(s));
+    fifo32_push(&s->tx_fifo, tx);
 
     if (s->phase != K230_DW_SSI_PHASE_STANDARD_TX_ONLY) {
         k230_dw_ssi_run_transfer(s);
@@ -444,6 +451,267 @@ static uint32_t k230_dw_ssi_send_frame(K230DwSsiState *s,
     return rx & mask;
 }
 
+static bool k230_dw_ssi_enhanced_config_supported(K230DwSsiState *s)
+{
+    uint32_t ctrlr0 = s->regs[R_CTRLR0];
+    uint32_t spi_ctrlr0 = s->regs[R_SPI_CTRLR0];
+    uint32_t spi_frf;
+    uint32_t trans_type;
+    uint32_t tmod;
+    uint32_t required_lines;
+
+    spi_frf = FIELD_EX32(ctrlr0, CTRLR0, SPI_FRF);
+    trans_type = FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, TRANS_TYPE);
+    tmod = FIELD_EX32(ctrlr0, CTRLR0, TMOD);
+
+    switch (spi_frf) {
+    case 1: /* Dual */
+        required_lines = 2;
+        break;
+    case 2: /* Quad */
+        required_lines = 4;
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: unsupported SPI_FRF=%u\n",
+                      DEVICE(s)->canonical_path, spi_frf);
+        return false;
+    }
+
+    if (required_lines > s->max_lines) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: SPI_FRF=%u requires %u lines, only %u available\n",
+                      DEVICE(s)->canonical_path,
+                      spi_frf, required_lines, s->max_lines);
+        return false;
+    }
+
+    if (trans_type > 2) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: unsupported TRANS_TYPE=%u\n",
+                      DEVICE(s)->canonical_path, trans_type);
+        return false;
+    }
+
+    if (tmod != K230_DW_SSI_TMOD_RO && tmod != K230_DW_SSI_TMOD_TO) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: unsupported enhanced TMOD=%u\n",
+                      DEVICE(s)->canonical_path, tmod);
+        return false;
+    }
+
+    if (FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, SPI_DDR_EN) ||
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, INST_DDR_EN) ||
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, SPI_RXDS_EN) ||
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, SPI_RXDS_SIG_EN)) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: DDR/RXDS enhanced mode is unsupported\n",
+                      DEVICE(s)->canonical_path);
+        return false;
+    }
+
+    return true;
+}
+
+static bool k230_dw_ssi_decode_enhanced_command(
+    K230DwSsiState *s, K230DwSsiEnhancedCommand *command)
+{
+    uint32_t spi_frf = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, SPI_FRF);
+    uint32_t inst_l = FIELD_EX32(s->regs[R_SPI_CTRLR0], SPI_CTRLR0, INST_L);
+    uint32_t addr_l = FIELD_EX32(s->regs[R_SPI_CTRLR0], SPI_CTRLR0, ADDR_L);
+    uint32_t trans_type = FIELD_EX32(s->regs[R_SPI_CTRLR0],
+                                     SPI_CTRLR0, TRANS_TYPE);
+    uint32_t inst_bits;
+    uint32_t addr_bits = addr_l << 2;
+    uint32_t mode_bits = 0;
+    bool mode_bits_enabled =
+        FIELD_EX32(s->regs[R_SPI_CTRLR0], SPI_CTRLR0, XIP_MD_BIT_EN);
+
+    if (!k230_dw_ssi_enhanced_config_supported(s)) {
+        return false;
+    }
+
+    inst_bits = inst_l ? (1U << (inst_l + 1)) : 0;
+
+    if (addr_bits > 32) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: unsupported enhanced address length %u bits\n",
+                      DEVICE(s)->canonical_path, addr_bits);
+        return false;
+    }
+
+    if (mode_bits_enabled) {
+        uint32_t mode_length_encoding =
+            FIELD_EX32(s->regs[R_SPI_CTRLR0], SPI_CTRLR0, XIP_MBL);
+
+        mode_bits = 1U << (mode_length_encoding + 1);
+    }
+
+    command->instruction_bits = inst_bits;
+    command->address_bits = addr_bits;
+    command->mode_bits = mode_bits;
+    command->mode_bits_enabled = mode_bits_enabled;
+    command->wait_cycles =
+        FIELD_EX32(s->regs[R_SPI_CTRLR0], SPI_CTRLR0, WAIT_CYCLES);
+    command->data_frames =
+        FIELD_EX32(s->regs[R_CTRLR1], CTRLR1, NDF) + 1;
+    command->spi_frf = spi_frf;
+    command->trans_type = trans_type;
+    command->tmod = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, TMOD);
+
+    if (mode_bits_enabled) {
+        command->mode = s->regs[R_XIP_MODE_BITS] &
+            (uint32_t)MAKE_64BIT_MASK(0, mode_bits);
+    }
+
+    return true;
+}
+
+static bool k230_dw_ssi_prepare_enhanced_command(K230DwSsiState *s)
+{
+    K230DwSsiEnhancedCommand command = { 0 };
+    uint32_t required_items;
+
+    if (!k230_dw_ssi_decode_enhanced_command(s, &command)) {
+        return false;
+    }
+
+    required_items = (command.instruction_bits != 0) +
+                     (command.address_bits != 0);
+    if (fifo32_num_used(&s->tx_fifo) < required_items) {
+        return false;
+    }
+
+    if (command.instruction_bits != 0) {
+        command.instruction = fifo32_pop(&s->tx_fifo) &
+            (uint32_t)MAKE_64BIT_MASK(0, command.instruction_bits);
+    }
+    if (command.address_bits != 0) {
+        command.address = fifo32_pop(&s->tx_fifo) &
+            (uint32_t)MAKE_64BIT_MASK(0, command.address_bits);
+    }
+
+    s->enhanced = command;
+    s->remaining_frames = command.data_frames;
+    s->phase = K230_DW_SSI_PHASE_ENHANCED_INSTRUCTION;
+    return true;
+}
+
+static void k230_dw_ssi_send_enhanced_field(K230DwSsiState *s,
+                                             uint32_t value,
+                                             uint32_t bits)
+{
+    uint32_t bytes = DIV_ROUND_UP(bits, 8);
+
+    for (uint32_t i = 0; i < bytes; i++) {
+        uint32_t shift = (bytes - i - 1) * 8;
+
+        ssi_transfer(s->spi, (value >> shift) & 0xff);
+    }
+}
+
+static uint32_t k230_dw_ssi_dummy_bytes(uint32_t spi_frf,
+                                         uint32_t trans_type,
+                                         uint32_t wait_cycles)
+{
+    uint32_t lines = 1;
+
+    if (trans_type != 0) {
+        lines = spi_frf == 1 ? 2 : 4;
+    }
+
+    return DIV_ROUND_UP(wait_cycles * lines, 8);
+}
+
+static void k230_dw_ssi_run_enhanced_rx_data(K230DwSsiState *s)
+{
+    while (!fifo32_is_full(&s->rx_fifo) &&
+           s->remaining_frames > 0) {
+        uint32_t rx = ssi_transfer(s->spi, 0);
+
+        fifo32_push(&s->rx_fifo,
+                    rx & k230_dw_ssi_frame_masked(s));
+        s->remaining_frames--;
+    }
+}
+
+static void k230_dw_ssi_run_enhanced_tx_data(K230DwSsiState *s)
+{
+    uint32_t mask = k230_dw_ssi_frame_masked(s);
+
+    while (!fifo32_is_empty(&s->tx_fifo) &&
+           s->remaining_frames > 0) {
+        uint32_t tx = fifo32_pop(&s->tx_fifo);
+
+        ssi_transfer(s->spi, tx & mask);
+        s->remaining_frames--;
+    }
+}
+
+static void k230_dw_ssi_run_enhanced_transfer(K230DwSsiState *s)
+{
+    if (s->phase == K230_DW_SSI_PHASE_IDLE) {
+        if (!k230_dw_ssi_prepare_enhanced_command(s)) {
+            return;
+        }
+    }
+
+    if (s->phase == K230_DW_SSI_PHASE_ENHANCED_INSTRUCTION) {
+        if (s->enhanced.instruction_bits != 0) {
+            k230_dw_ssi_send_enhanced_field(
+                s, s->enhanced.instruction,
+                s->enhanced.instruction_bits);
+        }
+        s->phase = K230_DW_SSI_PHASE_ENHANCED_ADDRESS;
+    }
+
+    if (s->phase == K230_DW_SSI_PHASE_ENHANCED_ADDRESS) {
+        if (s->enhanced.address_bits != 0) {
+            k230_dw_ssi_send_enhanced_field(
+                s, s->enhanced.address, s->enhanced.address_bits);
+        }
+        s->phase = K230_DW_SSI_PHASE_ENHANCED_MODE;
+    }
+
+    if (s->phase == K230_DW_SSI_PHASE_ENHANCED_MODE) {
+        if (s->enhanced.mode_bits_enabled) {
+            k230_dw_ssi_send_enhanced_field(
+                s, s->enhanced.mode, s->enhanced.mode_bits);
+        }
+        s->phase = K230_DW_SSI_PHASE_ENHANCED_DUMMY;
+    }
+
+    if (s->phase == K230_DW_SSI_PHASE_ENHANCED_DUMMY) {
+        uint32_t dummy_bytes = k230_dw_ssi_dummy_bytes(
+            s->enhanced.spi_frf, s->enhanced.trans_type,
+            s->enhanced.wait_cycles);
+
+        for (uint32_t i = 0; i < dummy_bytes; i++) {
+            ssi_transfer(s->spi, 0);
+        }
+        s->phase = K230_DW_SSI_PHASE_ENHANCED_DATA;
+    }
+
+    if (s->phase != K230_DW_SSI_PHASE_ENHANCED_DATA) {
+        g_assert_not_reached();
+    }
+
+    switch (s->enhanced.tmod) {
+    case K230_DW_SSI_TMOD_RO:
+        k230_dw_ssi_run_enhanced_rx_data(s);
+        break;
+    case K230_DW_SSI_TMOD_TO:
+        k230_dw_ssi_run_enhanced_tx_data(s);
+        break;
+    default:
+        g_assert_not_reached();
+    }
+
+    if (s->remaining_frames == 0) {
+        s->phase = K230_DW_SSI_PHASE_IDLE;
+    }
+}
+
 static void k230_dw_ssi_run_transfer(K230DwSsiState *s)
 {
     uint32_t spi_frf;
@@ -455,16 +723,18 @@ static void k230_dw_ssi_run_transfer(K230DwSsiState *s)
 
     spi_frf = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, SPI_FRF);
     if (spi_frf != 0) {
+        k230_dw_ssi_run_enhanced_transfer(s);
         return;
     }
 
     tmod = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, TMOD);
 
     switch (tmod) {
-    case 0: /* TX_AND_RX */
+    case K230_DW_SSI_TMOD_TR:
         while (!fifo32_is_empty(&s->tx_fifo)) {
             uint32_t tx = fifo32_pop(&s->tx_fifo);
             uint32_t rx = k230_dw_ssi_send_frame(s, tx);
+
             if (!fifo32_is_full(&s->rx_fifo)) {
                 fifo32_push(&s->rx_fifo, rx);
             } else {
@@ -476,7 +746,7 @@ static void k230_dw_ssi_run_transfer(K230DwSsiState *s)
             }
         }
         break;
-    case 1: { /* TX_ONLY */
+    case K230_DW_SSI_TMOD_TO: {
         unsigned int frames = 0;
 
         if (fifo32_is_empty(&s->tx_fifo)) {
@@ -497,7 +767,7 @@ static void k230_dw_ssi_run_transfer(K230DwSsiState *s)
         }
         break;
     }
-    case 2: /* RX_ONLY */
+    case K230_DW_SSI_TMOD_RO:
         switch (s->phase) {
         case K230_DW_SSI_PHASE_IDLE:
             if (fifo32_is_empty(&s->tx_fifo)) {
@@ -522,7 +792,7 @@ static void k230_dw_ssi_run_transfer(K230DwSsiState *s)
             break;
         }
         break;
-    case 3: /* EEPROM_READ */
+    case K230_DW_SSI_TMOD_EEPROM_READ:
         switch (s->phase) {
         case K230_DW_SSI_PHASE_IDLE:
             if (fifo32_is_empty(&s->tx_fifo)) {
@@ -562,8 +832,6 @@ static void k230_dw_ssi_run_transfer(K230DwSsiState *s)
     }
 }
 
-
-
 static bool k230_dw_ssi_is_dr(hwaddr addr)
 {
     return addr >= A_DR0 && addr <= A_DR_END &&
@@ -920,6 +1188,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;
+    memset(&s->enhanced, 0, sizeof(s->enhanced));
 
     s->regs[R_CTRLR0] = K230_DW_SSI_CTRLR0_RESET;
     s->regs[R_SR] = K230_DW_SSI_SR_RESET;
@@ -983,6 +1252,18 @@ static const VMStateDescription vmstate_k230_dw_ssi = {
         VMSTATE_UINT32(irq_latched, K230DwSsiState),
         VMSTATE_UINT32(phase, K230DwSsiState),
         VMSTATE_UINT32(remaining_frames, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.instruction, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.address, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.mode, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.mode_bits, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.instruction_bits, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.address_bits, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.wait_cycles, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.data_frames, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.spi_frf, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.trans_type, K230DwSsiState),
+        VMSTATE_UINT32(enhanced.tmod, K230DwSsiState),
+        VMSTATE_BOOL(enhanced.mode_bits_enabled, K230DwSsiState),
         VMSTATE_INT32(active_cs, K230DwSsiState),
         VMSTATE_END_OF_LIST()
     },
diff --git a/include/hw/ssi/k230_dw_ssi.h b/include/hw/ssi/k230_dw_ssi.h
index 404215245b..b692a616d6 100644
--- a/include/hw/ssi/k230_dw_ssi.h
+++ b/include/hw/ssi/k230_dw_ssi.h
@@ -51,8 +51,29 @@ typedef enum K230DwSsiPhase {
     K230_DW_SSI_PHASE_RX_ONLY,
     K230_DW_SSI_PHASE_EEPROM_COMMAND,
     K230_DW_SSI_PHASE_EEPROM_DATA,
+
+    K230_DW_SSI_PHASE_ENHANCED_INSTRUCTION,
+    K230_DW_SSI_PHASE_ENHANCED_ADDRESS,
+    K230_DW_SSI_PHASE_ENHANCED_MODE,
+    K230_DW_SSI_PHASE_ENHANCED_DUMMY,
+    K230_DW_SSI_PHASE_ENHANCED_DATA,
 } K230DwSsiPhase;
 
+typedef struct K230DwSsiEnhancedCommand {
+    uint32_t instruction;
+    uint32_t address;
+    uint32_t mode;
+    uint32_t instruction_bits;
+    uint32_t address_bits;
+    uint32_t mode_bits;
+    uint32_t wait_cycles;
+    uint32_t data_frames;
+    uint32_t spi_frf;
+    uint32_t trans_type;
+    uint32_t tmod;
+    bool mode_bits_enabled;
+} K230DwSsiEnhancedCommand;
+
 struct K230DwSsiState {
     SysBusDevice parent_obj;
 
@@ -69,6 +90,7 @@ struct K230DwSsiState {
 
     uint32_t phase;
     uint32_t remaining_frames;
+    K230DwSsiEnhancedCommand enhanced;
 
     uint32_t num_cs;
     uint32_t max_lines;
diff --git a/tests/qtest/k230-dw-ssi-test.c b/tests/qtest/k230-dw-ssi-test.c
index e6c0e2ee8c..a507814b66 100644
--- a/tests/qtest/k230-dw-ssi-test.c
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -55,12 +55,23 @@
 #define K230_SSI_CTRLR0_DFS_MASK        0x1fU
 #define K230_SSI_CTRLR0_TMOD_SHIFT      10
 #define K230_SSI_CTRLR0_SRL             BIT(13)
+#define K230_SSI_CTRLR0_SPI_FRF_SHIFT   22
+#define K230_SSI_CTRLR0_SPI_FRF_MASK    (3U << K230_SSI_CTRLR0_SPI_FRF_SHIFT)
 
 #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_FRF_QUAD               2
+#define K230_SSI_FRF_OCTAL              3
+
+#define K230_SSI_SPI_CTRLR0_TRANS_TYPE(v) ((v) & 0x3U)
+#define K230_SSI_SPI_CTRLR0_ADDR_L(bits)  (((bits) / 4U) << 2)
+#define K230_SSI_SPI_CTRLR0_INST_L_8      (2U << 8)
+#define K230_SSI_SPI_CTRLR0_WAIT(v)       (((v) & 0x1fU) << 11)
+#define K230_SSI_SPI_CTRLR0_SPI_DDR_EN    BIT(16)
+
 #define K230_SSI_SR_BUSY                BIT(0)
 #define K230_SSI_SR_TFNF                BIT(1)
 #define K230_SSI_SR_TFE                 BIT(2)
@@ -104,6 +115,8 @@ static const K230SsiInstance k230_ssi_instances[3] = {
     },
 };
 
+#define FLASH_CMD_JEDEC         0x9f
+
 static QTestState *k230_ssi_start(void)
 {
     return qtest_init("-machine k230");
@@ -326,6 +339,57 @@ static void test_plic_routing(void)
     qtest_quit(qts);
 }
 
+static void assert_enhanced_config_rejected(QTestState *qts, uint32_t frf,
+                                            uint32_t extra_spi_ctrlr0)
+{
+    uint32_t ctrlr0;
+    uint32_t spi_ctrlr0;
+
+    k230_ssi_configure(qts, K230_SPI0_BASE, K230_SSI_TMOD_RO, 8, 3);
+    ctrlr0 = k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_CTRLR0);
+    ctrlr0 |= frf << K230_SSI_CTRLR0_SPI_FRF_SHIFT;
+    spi_ctrlr0 = K230_SSI_SPI_CTRLR0_INST_L_8 | extra_spi_ctrlr0;
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_CTRLR0, ctrlr0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SPI_CTRLR0, spi_ctrlr0);
+    k230_ssi_enable_cs(qts, K230_SPI0_BASE, BIT(0));
+    k230_ssi_write_frame(qts, K230_SPI0_BASE, FLASH_CMD_JEDEC);
+    g_assert_cmpuint(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_TXFLR),
+                     ==, 1);
+    g_assert_cmpuint(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_RXFLR),
+                     ==, 0);
+    k230_ssi_disable(qts, K230_SPI0_BASE);
+}
+
+static void test_qspi_config(void)
+{
+    QTestState *qts = k230_ssi_start();
+    uint32_t ctrlr0;
+    uint32_t spi_ctrlr0;
+
+    assert_enhanced_config_rejected(qts, K230_SSI_FRF_OCTAL, 0);
+    assert_enhanced_config_rejected(qts, K230_SSI_FRF_QUAD,
+                                    K230_SSI_SPI_CTRLR0_SPI_DDR_EN);
+
+    k230_ssi_configure(qts, K230_SPI0_BASE, K230_SSI_TMOD_RO, 8, 3);
+    ctrlr0 = k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_CTRLR0);
+    ctrlr0 |= K230_SSI_FRF_QUAD << K230_SSI_CTRLR0_SPI_FRF_SHIFT;
+    spi_ctrlr0 = K230_SSI_SPI_CTRLR0_TRANS_TYPE(1) |
+                 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_CTRLR0, ctrlr0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SPI_CTRLR0, spi_ctrlr0);
+    k230_ssi_enable_cs(qts, K230_SPI0_BASE, BIT(0));
+    k230_ssi_write_frame(qts, K230_SPI0_BASE, FLASH_CMD_JEDEC);
+    k230_ssi_write_frame(qts, K230_SPI0_BASE, 0x123456);
+    g_assert_cmpuint(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_RXFLR),
+                     ==, 4);
+    for (int i = 0; i < 4; i++) {
+        g_assert_cmphex(k230_ssi_read_frame(qts, K230_SPI0_BASE), ==, 0);
+    }
+    qtest_quit(qts);
+}
+
 int main(int argc, char **argv)
 {
     g_test_init(&argc, &argv, NULL);
@@ -335,5 +399,6 @@ int main(int argc, char **argv)
     qtest_add_func("/k230-dw-ssi/interrupt-controller",
                    test_interrupt_controller);
     qtest_add_func("/k230-dw-ssi/plic-routing", test_plic_routing);
+    qtest_add_func("/k230-dw-ssi/qspi-config", test_qspi_config);
     return g_test_run();
 }
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 07/11] hw/riscv/k230: Attach SPI NOR flash to spi0
  2026-07-26 12:28 ` Kangjie Huang
                   ` (6 preceding siblings ...)
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Add the optional spi-flash machine property and attach the selected
m25p80-compatible device to logical spi0 chip select 0.

Use an MTD backend when supplied and keep the erased-flash default when
no drive is configured.

Cover JEDEC identification, standard read and page program, Quad output
read, and Quad page program in qtest.

Also exercise standard SPI reads and writes from U-Boot and Linux.

Also verify the U-Boot-managed boot path by loading the Linux and OpenSBI
payloads from the attached SPI flash instead of injecting them via the
QEMU command line. The system reaches a Linux shell.

Signed-off-by: Kangjie Huang <flamboyant.h.01@gmail.com>
---
 docs/system/riscv/k230.rst     |   1 +
 hw/riscv/Kconfig               |   1 +
 hw/riscv/k230.c                |  66 ++++++++
 include/hw/riscv/k230.h        |   2 +
 tests/qtest/k230-dw-ssi-test.c | 285 +++++++++++++++++++++++++++++++++
 5 files changed, 355 insertions(+)

diff --git a/docs/system/riscv/k230.rst b/docs/system/riscv/k230.rst
index 0358709d2c..0d2617c0f9 100644
--- a/docs/system/riscv/k230.rst
+++ b/docs/system/riscv/k230.rst
@@ -21,6 +21,7 @@ The ``k230`` machine supports the following devices:
 * 2 K230 Watchdog Timer
 * 5 UART
 * 3 K230 SSI controllers for SPI and QSPI
+* Optional SPI NOR flash on spi0 CS0
 
 Boot options
 ------------
diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig
index 69059ae044..4984b0c9f3 100644
--- a/hw/riscv/Kconfig
+++ b/hw/riscv/Kconfig
@@ -163,3 +163,4 @@ config K230
     select UNIMP
     select K230_WDT
     select K230_DW_SSI
+    select SSI_M25P80
diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
index 1863d5ecbb..36fd1eda22 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"
@@ -31,6 +33,7 @@
 #include "hw/intc/sifive_plic.h"
 #include "hw/char/serial-mm.h"
 #include "hw/misc/unimp.h"
+#include "hw/ssi/ssi.h"
 
 /* Align K230_SDK k230_canmv_defconfig */
 #define K230_DIRECT_OPENSBI_ADDR 0x8000000
@@ -512,6 +515,49 @@ static void k230_firmware_boot(K230MachineState *s, MachineState *machine)
                               memmap[K230_DEV_BOOTROM].size, 0, 0);
 }
 
+static char *k230_machine_get_spi_flash(Object *obj, Error **errp)
+{
+    K230MachineState *s = RISCV_K230_MACHINE(obj);
+
+    return g_strdup(s->spi_flash_model);
+}
+
+static void k230_machine_set_spi_flash(Object *obj, const char *value,
+                                       Error **errp)
+{
+    K230MachineState *s = RISCV_K230_MACHINE(obj);
+
+    g_free(s->spi_flash_model);
+    s->spi_flash_model = g_strdup(value);
+}
+
+static void k230_connect_spi_flash(K230DwSsiState *ssi, unsigned int cs,
+                                   const char *flash_type, DriveInfo *dinfo)
+{
+    ObjectClass *flash_class;
+    DeviceState *flash;
+    qemu_irq flash_cs;
+
+    flash_class = module_object_class_by_name(flash_type);
+    if (!flash_class || object_class_is_abstract(flash_class) ||
+        !object_class_dynamic_cast(flash_class, TYPE_M25P80)) {
+        error_report("'%s' is either abstract or not a subtype of m25p80",
+                     flash_type);
+        exit(EXIT_FAILURE);
+    }
+
+    flash = qdev_new(flash_type);
+
+    if (dinfo) {
+        qdev_prop_set_drive(flash, "drive", blk_by_legacy_dinfo(dinfo));
+    }
+
+    qdev_realize_and_unref(flash, BUS(ssi->spi), &error_fatal);
+
+    flash_cs = qdev_get_gpio_in_named(flash, SSI_GPIO_CS, 0);
+    qdev_connect_gpio_out_named(DEVICE(ssi), "cs", cs, flash_cs);
+}
+
 static void k230_machine_done(Notifier *notifier, void *data)
 {
     K230MachineState *s = container_of(notifier, K230MachineState,
@@ -543,6 +589,12 @@ static void k230_machine_init(MachineState *machine)
                             TYPE_RISCV_K230_SOC);
     qdev_realize(DEVICE(&s->soc), NULL, &error_fatal);
 
+    if (s->spi_flash_model) {
+        k230_connect_spi_flash(&s->soc.dw_ssi[2], 0,
+                               s->spi_flash_model,
+                               drive_get(IF_MTD, 0, 0));
+    }
+
     /* Data Memory */
     memory_region_add_subregion(sys_mem, memmap[K230_DEV_DDRC].base,
                                 machine->ram);
@@ -555,6 +607,13 @@ static void k230_machine_instance_init(Object *obj)
 {
 }
 
+static void k230_machine_instance_finalize(Object *obj)
+{
+    K230MachineState *s = RISCV_K230_MACHINE(obj);
+
+    g_clear_pointer(&s->spi_flash_model, g_free);
+}
+
 static void k230_machine_class_init(ObjectClass *oc, const void *data)
 {
     MachineClass *mc = MACHINE_CLASS(oc);
@@ -564,6 +623,12 @@ 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;
+
+    object_class_property_add_str(oc, "spi-flash",
+                                  k230_machine_get_spi_flash,
+                                  k230_machine_set_spi_flash);
+    object_class_property_set_description(
+        oc, "spi-flash", "Attach an M25P80-compatible flash to spi0 CS0");
 }
 
 static const TypeInfo k230_machine_typeinfo = {
@@ -571,6 +636,7 @@ static const TypeInfo k230_machine_typeinfo = {
     .parent     = TYPE_MACHINE,
     .class_init = k230_machine_class_init,
     .instance_init = k230_machine_instance_init,
+    .instance_finalize = k230_machine_instance_finalize,
     .instance_size = sizeof(K230MachineState),
     .interfaces = riscv64_machine_interfaces,
 };
diff --git a/include/hw/riscv/k230.h b/include/hw/riscv/k230.h
index cfe30590cc..493d739aa4 100644
--- a/include/hw/riscv/k230.h
+++ b/include/hw/riscv/k230.h
@@ -52,6 +52,8 @@ typedef struct K230MachineState {
     /*< public >*/
     K230SoCState soc;
     Notifier machine_done;
+
+    char *spi_flash_model;
 } K230MachineState;
 
 enum {
diff --git a/tests/qtest/k230-dw-ssi-test.c b/tests/qtest/k230-dw-ssi-test.c
index a507814b66..658b1f4cbf 100644
--- a/tests/qtest/k230-dw-ssi-test.c
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -9,6 +9,7 @@
 #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
@@ -68,9 +69,11 @@
 
 #define K230_SSI_SPI_CTRLR0_TRANS_TYPE(v) ((v) & 0x3U)
 #define K230_SSI_SPI_CTRLR0_ADDR_L(bits)  (((bits) / 4U) << 2)
+#define K230_SSI_SPI_CTRLR0_XIP_MD_EN     BIT(7)
 #define K230_SSI_SPI_CTRLR0_INST_L_8      (2U << 8)
 #define K230_SSI_SPI_CTRLR0_WAIT(v)       (((v) & 0x1fU) << 11)
 #define K230_SSI_SPI_CTRLR0_SPI_DDR_EN    BIT(16)
+#define K230_SSI_SPI_CTRLR0_XIP_MBL_8     (2U << 26)
 
 #define K230_SSI_SR_BUSY                BIT(0)
 #define K230_SSI_SR_TFNF                BIT(1)
@@ -88,6 +91,10 @@
 #define K230_SSI_IRQ_AXIE               8
 
 #define K230_SSI_FIFO_DEPTH             256
+#define K230_SSI_FLASH_IMAGE_SIZE       (32 * MiB)
+#define K230_SSI_FLASH_PATTERN_ADDR     0x100
+#define K230_SSI_FLASH_HIGH_ADDR        0x1000100
+#define K230_SSI_FLASH_PROGRAM_ADDR     0x22000
 
 typedef struct K230SsiInstance {
     uint64_t base;
@@ -96,6 +103,10 @@ typedef struct K230SsiInstance {
     uint32_t first_irq;
 } K230SsiInstance;
 
+typedef struct K230SsiFlashImage {
+    char *path;
+} K230SsiFlashImage;
+
 static const K230SsiInstance k230_ssi_instances[3] = {
     {
         .base = K230_SPI0_BASE,
@@ -117,6 +128,17 @@ static const K230SsiInstance k230_ssi_instances[3] = {
 
 #define FLASH_CMD_JEDEC         0x9f
 
+#define FLASH_CMD_WREN          0x06
+#define FLASH_CMD_RDSR          0x05
+#define FLASH_CMD_READ          0x03
+#define FLASH_CMD_READ4         0x13
+#define FLASH_CMD_QUAD_OUT      0x6b
+#define FLASH_CMD_QUAD_IO       0xeb
+#define FLASH_CMD_PP            0x02
+#define FLASH_SR_WIP            BIT(0)
+
+#define FLASH_CMD_QUAD_PP       0x32
+
 static QTestState *k230_ssi_start(void)
 {
     return qtest_init("-machine k230");
@@ -210,6 +232,188 @@ static bool k230_ssi_plic_pending(QTestState *qts, uint32_t irq)
     return qtest_readl(qts, addr) & BIT(irq % 32);
 }
 
+static void write_exact(int fd, const void *buf, size_t len, off_t offset)
+{
+    ssize_t ret = pwrite(fd, buf, len, offset);
+
+    g_assert_cmpint(ret, ==, len);
+}
+
+static void k230_ssi_flash_image_init(K230SsiFlashImage *image)
+{
+    static const uint8_t low_pattern[] = {
+        0xa5, 0x5a, 0x3c, 0xc3, 0x11, 0x22, 0x33, 0x44,
+    };
+    static const uint8_t high_pattern[] = { 0x71, 0x72, 0x73, 0x74 };
+    uint8_t erased[4096];
+    int fd;
+
+    memset(erased, 0xff, sizeof(erased));
+    image->path = NULL;
+    fd = g_file_open_tmp("qtest.k230.w25q256.XXXXXX", &image->path, NULL);
+    g_assert_cmpint(fd, >=, 0);
+    g_assert_cmpint(ftruncate(fd, K230_SSI_FLASH_IMAGE_SIZE), ==, 0);
+    write_exact(fd, low_pattern, sizeof(low_pattern),
+                K230_SSI_FLASH_PATTERN_ADDR);
+    write_exact(fd, high_pattern, sizeof(high_pattern),
+                K230_SSI_FLASH_HIGH_ADDR);
+    write_exact(fd, erased, sizeof(erased), K230_SSI_FLASH_PROGRAM_ADDR);
+    close(fd);
+}
+
+static void k230_ssi_flash_image_clear(K230SsiFlashImage *image)
+{
+    if (image->path) {
+        unlink(image->path);
+        g_clear_pointer(&image->path, g_free);
+    }
+}
+
+static QTestState *k230_ssi_start_with_flash(K230SsiFlashImage *image)
+{
+    k230_ssi_flash_image_init(image);
+    return qtest_initf("-machine k230,spi-flash=w25q256 "
+                       "-drive file=%s,format=raw,if=mtd",
+                       image->path);
+}
+
+static void flash_write_transaction(QTestState *qts,
+                                    const uint8_t *command,
+                                    size_t command_len)
+{
+    g_assert_nonnull(command);
+    g_assert_cmpuint(command_len, >, 0);
+    g_assert_cmpuint(command_len, <=, K230_SSI_FIFO_DEPTH);
+
+    k230_ssi_configure(qts, K230_SPI0_BASE, K230_SSI_TMOD_TO, 8, 0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, 0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SSIENR, 1);
+    for (size_t i = 0; i < command_len; i++) {
+        k230_ssi_write_frame(qts, K230_SPI0_BASE, command[i]);
+    }
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, BIT(0));
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_TXFLR,
+                       UINT32_MAX, 0);
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_SR,
+                       K230_SSI_SR_BUSY, 0);
+    k230_ssi_disable(qts, K230_SPI0_BASE);
+}
+
+static void flash_read_transaction(QTestState *qts,
+                                   const uint8_t *command,
+                                   size_t command_len,
+                                   uint8_t *data, size_t data_len)
+{
+    g_assert_nonnull(command);
+    g_assert_nonnull(data);
+    g_assert_cmpuint(command_len, >, 0);
+    g_assert_cmpuint(data_len, >, 0);
+    g_assert_cmpuint(data_len, <, K230_SSI_FIFO_DEPTH);
+
+    k230_ssi_configure(qts, K230_SPI0_BASE,
+                       K230_SSI_TMOD_EEPROM_READ, 8, data_len - 1);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, 0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SSIENR, 1);
+    for (size_t i = 0; i < command_len; i++) {
+        k230_ssi_write_frame(qts, K230_SPI0_BASE, command[i]);
+    }
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, BIT(0));
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_RXFLR,
+                       UINT32_MAX, data_len);
+    for (size_t i = 0; i < data_len; i++) {
+        data[i] = k230_ssi_read_frame(qts, K230_SPI0_BASE);
+    }
+    k230_ssi_disable(qts, K230_SPI0_BASE);
+}
+
+static void flash_read(QTestState *qts, uint8_t opcode, uint32_t address,
+                       unsigned int addr_bytes, uint8_t *data, size_t len)
+{
+    uint8_t command[5];
+
+    g_assert_cmpuint(addr_bytes, <=, 4);
+    command[0] = opcode;
+    for (unsigned int i = 0; i < addr_bytes; i++) {
+        command[1 + i] = address >> (8 * (addr_bytes - i - 1));
+    }
+    flash_read_transaction(qts, command, 1 + addr_bytes, data, len);
+}
+
+static uint8_t flash_read_status(QTestState *qts)
+{
+    uint8_t command = FLASH_CMD_RDSR;
+    uint8_t status;
+
+    flash_read_transaction(qts, &command, 1, &status, 1);
+    return status;
+}
+
+static void flash_wait_ready(QTestState *qts)
+{
+    for (int i = 0; i < 1000; i++) {
+        if (!(flash_read_status(qts) & FLASH_SR_WIP)) {
+            return;
+        }
+        qtest_clock_step(qts, 1000000);
+    }
+    g_assert_cmphex(flash_read_status(qts) & FLASH_SR_WIP, ==, 0);
+}
+
+static void flash_write_enable(QTestState *qts)
+{
+    uint8_t command = FLASH_CMD_WREN;
+
+    flash_write_transaction(qts, &command, 1);
+}
+
+static void configure_enhanced_transfer(QTestState *qts, uint32_t tmod,
+                                        uint32_t frf, uint32_t trans_type,
+                                        uint32_t wait_cycles,
+                                        bool mode_bits_enabled,
+                                        size_t data_frames)
+{
+    uint32_t ctrlr0;
+    uint32_t spi_ctrlr0;
+
+    k230_ssi_configure(qts, K230_SPI0_BASE, tmod, 8, data_frames - 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_CTRLR0_SPI_FRF_MASK;
+    ctrlr0 |= frf << 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(trans_type) |
+                 K230_SSI_SPI_CTRLR0_ADDR_L(24) |
+                 K230_SSI_SPI_CTRLR0_INST_L_8 |
+                 K230_SSI_SPI_CTRLR0_WAIT(wait_cycles);
+    if (mode_bits_enabled) {
+        spi_ctrlr0 |= K230_SSI_SPI_CTRLR0_XIP_MD_EN |
+                      K230_SSI_SPI_CTRLR0_XIP_MBL_8;
+    }
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SPI_CTRLR0, spi_ctrlr0);
+}
+
+static void start_enhanced_transfer(QTestState *qts, uint8_t opcode,
+                                    uint32_t address)
+{
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SSIENR, 1);
+    k230_ssi_write_frame(qts, K230_SPI0_BASE, opcode);
+    k230_ssi_write_frame(qts, K230_SPI0_BASE, address);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, BIT(0));
+}
+
+static void read_enhanced_result(QTestState *qts, uint8_t *data, size_t len)
+{
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_RXFLR,
+                       UINT32_MAX, len);
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_SR,
+                       K230_SSI_SR_BUSY, 0);
+    for (size_t i = 0; i < len; i++) {
+        data[i] = k230_ssi_read_frame(qts, K230_SPI0_BASE);
+    }
+    k230_ssi_disable(qts, K230_SPI0_BASE);
+}
+
 static void test_register_contract(void)
 {
     QTestState *qts = k230_ssi_start();
@@ -390,6 +594,85 @@ static void test_qspi_config(void)
     qtest_quit(qts);
 }
 
+static void test_spi_nor(void)
+{
+    static const uint8_t expected[] = {
+        0xa5, 0x5a, 0x3c, 0xc3, 0x11, 0x22, 0x33, 0x44,
+    };
+    static const uint8_t payload[] = { 0xde, 0xad, 0xbe, 0xef };
+    K230SsiFlashImage image;
+    QTestState *qts = k230_ssi_start_with_flash(&image);
+    uint8_t command = FLASH_CMD_JEDEC;
+    uint8_t id[3];
+    uint8_t actual[ARRAY_SIZE(expected)];
+    uint8_t program[4 + ARRAY_SIZE(payload)];
+    uint32_t addr = K230_SSI_FLASH_PROGRAM_ADDR;
+
+    flash_read_transaction(qts, &command, 1, id, sizeof(id));
+    g_assert_cmphex(id[0], ==, 0xef);
+    g_assert_cmphex(id[1], ==, 0x40);
+    g_assert_cmphex(id[2], ==, 0x19);
+    flash_read(qts, FLASH_CMD_READ, K230_SSI_FLASH_PATTERN_ADDR,
+               3, actual, sizeof(actual));
+    g_assert_cmpmem(actual, sizeof(actual), expected, sizeof(expected));
+
+    flash_write_enable(qts);
+    program[0] = FLASH_CMD_PP;
+    program[1] = addr >> 16;
+    program[2] = addr >> 8;
+    program[3] = addr;
+    memcpy(program + 4, payload, sizeof(payload));
+    flash_write_transaction(qts, program, sizeof(program));
+    flash_wait_ready(qts);
+    flash_read(qts, FLASH_CMD_READ, addr, 3, actual, sizeof(payload));
+    g_assert_cmpmem(actual, sizeof(payload), payload, sizeof(payload));
+
+    qtest_quit(qts);
+    k230_ssi_flash_image_clear(&image);
+}
+
+static void test_qspi_sdr(void)
+{
+    static const uint8_t expected[] = { 0xa5, 0x5a, 0x3c, 0xc3 };
+    static const uint8_t payload[] = { 0x12, 0x34, 0x56, 0x78 };
+    K230SsiFlashImage image;
+    QTestState *qts = k230_ssi_start_with_flash(&image);
+    uint8_t actual[ARRAY_SIZE(expected)];
+
+    configure_enhanced_transfer(qts, K230_SSI_TMOD_RO,
+                                K230_SSI_FRF_QUAD, 0, 8, false,
+                                ARRAY_SIZE(actual));
+    start_enhanced_transfer(qts, FLASH_CMD_QUAD_OUT,
+                            K230_SSI_FLASH_PATTERN_ADDR);
+    read_enhanced_result(qts, actual, sizeof(actual));
+    g_assert_cmpmem(actual, sizeof(actual), expected, sizeof(expected));
+
+    flash_write_enable(qts);
+    configure_enhanced_transfer(qts, K230_SSI_TMOD_TO,
+                                K230_SSI_FRF_QUAD, 0, 0, false,
+                                ARRAY_SIZE(payload));
+    start_enhanced_transfer(qts, FLASH_CMD_QUAD_PP,
+                            K230_SSI_FLASH_PROGRAM_ADDR);
+    for (int i = 0; i < ARRAY_SIZE(payload); i++) {
+        k230_ssi_write_frame(qts, K230_SPI0_BASE, payload[i]);
+    }
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_SR,
+                       K230_SSI_SR_BUSY, 0);
+    k230_ssi_disable(qts, K230_SPI0_BASE);
+    flash_wait_ready(qts);
+
+    configure_enhanced_transfer(qts, K230_SSI_TMOD_RO,
+                                K230_SSI_FRF_QUAD, 0, 8, false,
+                                ARRAY_SIZE(actual));
+    start_enhanced_transfer(qts, FLASH_CMD_QUAD_OUT,
+                            K230_SSI_FLASH_PROGRAM_ADDR);
+    read_enhanced_result(qts, actual, sizeof(actual));
+    g_assert_cmpmem(actual, sizeof(payload), payload, sizeof(payload));
+
+    qtest_quit(qts);
+    k230_ssi_flash_image_clear(&image);
+}
+
 int main(int argc, char **argv)
 {
     g_test_init(&argc, &argv, NULL);
@@ -400,5 +683,7 @@ int main(int argc, char **argv)
                    test_interrupt_controller);
     qtest_add_func("/k230-dw-ssi/plic-routing", test_plic_routing);
     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);
     return g_test_run();
 }
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 08/11] hw/ssi: Implement K230 SSI internal DMA transfers
  2026-07-26 12:28 ` Kangjie Huang
                   ` (7 preceding siblings ...)
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

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 <flamboyant.h.01@gmail.com>
---
 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



^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 09/11] hw/misc: Add K230 HI_SYS SSI control
  2026-07-26 12:28 ` Kangjie Huang
                   ` (8 preceding siblings ...)
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Model the HI_SYS SSI_CTRL wrapper register, including its reset value,
write mask, and dynamic mode and sleep status for the three logical SSI
controllers.

K230 software uses this register to control XIP enable and observe the
mode and sleep state of the SSI instances, so controller-local registers
alone do not provide the complete guest-visible interface.

Reuse the machine SSI routing table to associate logical controller
numbers with physical instances. Keep the sleep indication synchronized
when IDMA disables SSI after completion or an AXI error.

Map the wrapper over the previously unimplemented HI_SYS region and
migrate its writable state and the controller sleep state. Cover the
register reset value, write mask, and read-back behaviour, three-instance
mode routing, and sleep transitions in qtest.

Signed-off-by: Kangjie Huang <flamboyant.h.01@gmail.com>
---
 hw/misc/k230_hi_sys.c          | 164 +++++++++++++++++++++++++++++++++
 hw/misc/meson.build            |   1 +
 hw/riscv/k230.c                |  19 +++-
 hw/ssi/k230_dw_ssi.c           |  15 +++
 include/hw/misc/k230_hi_sys.h  |  54 +++++++++++
 include/hw/riscv/k230.h        |   2 +
 include/hw/ssi/k230_dw_ssi.h   |   4 +
 tests/qtest/k230-dw-ssi-test.c |  60 ++++++++++++
 8 files changed, 316 insertions(+), 3 deletions(-)
 create mode 100644 hw/misc/k230_hi_sys.c
 create mode 100644 include/hw/misc/k230_hi_sys.h

diff --git a/hw/misc/k230_hi_sys.c b/hw/misc/k230_hi_sys.c
new file mode 100644
index 0000000000..44f5ac61de
--- /dev/null
+++ b/hw/misc/k230_hi_sys.c
@@ -0,0 +1,164 @@
+/*
+ * Kendryte K230 HI_SYS
+ *
+ * Copyright (c) 2026 Kangjie Huang <flamboyant.h.01@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Models the HI_SYS system control block, including the SSI_CTRL wrapper
+ * register that K230 firmware uses for XIP enable and SSI mode/sleep
+ * status.
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18):
+ * https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
+ *
+ * For more information, see <https://www.kendryte.com/en/proDetail/230>
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "hw/misc/k230_hi_sys.h"
+#include "migration/vmstate.h"
+
+/*
+ * The TRM places SSI_CTRL at HI_SYS_CONFIG + 0x068, while the DWC SSI
+ * register table uses controller offset 0x068 for DR2. These are separate
+ * address spaces, so model SSI_CTRL in HI_SYS rather than as an SSI DR alias.
+ */
+static uint32_t k230_hi_sys_ssi_status(const K230HiSysState *s)
+{
+    static const unsigned int sleep_bits[] = {
+        K230_SSI_CTRL_SPI0_SLEEP,
+        K230_SSI_CTRL_SPI1_SLEEP,
+        K230_SSI_CTRL_SPI2_SLEEP,
+    };
+    static const unsigned int mode_shifts[] = {
+        K230_SSI_CTRL_SPI0_MODE_SHIFT,
+        K230_SSI_CTRL_SPI1_MODE_SHIFT,
+        K230_SSI_CTRL_SPI2_MODE_SHIFT,
+    };
+    uint32_t value = s->ssi_ctrl;
+
+    for (unsigned int i = 0; i < ARRAY_SIZE(s->ssi); i++) {
+        if (!s->ssi[i]) {
+            continue;
+        }
+
+        value |= k230_dw_ssi_get_spi_mode(s->ssi[i]) << mode_shifts[i];
+        if (k230_dw_ssi_is_sleeping(s->ssi[i])) {
+            value |= sleep_bits[i];
+        }
+    }
+
+    return value;
+}
+
+static uint64_t k230_hi_sys_read(void *opaque, hwaddr addr, unsigned size)
+{
+    K230HiSysState *s = opaque;
+
+    if (addr == K230_HI_SYS_SSI_CTRL_OFFSET) {
+        return k230_hi_sys_ssi_status(s);
+    }
+
+    if (addr < K230_HI_SYS_MMIO_SIZE) {
+        return 0;
+    }
+
+    qemu_log_mask(LOG_GUEST_ERROR,
+                  "%s: read from invalid offset 0x%" HWADDR_PRIx "\n",
+                  TYPE_K230_HI_SYS, addr);
+    return 0;
+}
+
+static void k230_hi_sys_write(void *opaque, hwaddr addr, uint64_t value,
+                              unsigned size)
+{
+    K230HiSysState *s = opaque;
+
+    if (addr == K230_HI_SYS_SSI_CTRL_OFFSET) {
+        s->ssi_ctrl = (s->ssi_ctrl & ~K230_SSI_CTRL_WRITABLE_MASK) |
+                      ((uint32_t)value & K230_SSI_CTRL_WRITABLE_MASK);
+        s->ssi_ctrl &= K230_SSI_CTRL_IMPLEMENTED_MASK;
+        return;
+    }
+
+    if (addr < K230_HI_SYS_MMIO_SIZE) {
+        return;
+    }
+
+    qemu_log_mask(LOG_GUEST_ERROR,
+                  "%s: write to invalid offset 0x%" HWADDR_PRIx "\n",
+                  TYPE_K230_HI_SYS, addr);
+}
+
+static const MemoryRegionOps k230_hi_sys_ops = {
+    .read = k230_hi_sys_read,
+    .write = k230_hi_sys_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+    .impl = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+};
+
+void k230_hi_sys_set_ssi(K230HiSysState *s, unsigned int index,
+                         K230DwSsiState *ssi)
+{
+    g_assert(index < ARRAY_SIZE(s->ssi));
+    s->ssi[index] = ssi;
+}
+
+static void k230_hi_sys_reset(Object *obj, ResetType type)
+{
+    K230HiSysState *s = K230_HI_SYS(obj);
+
+    s->ssi_ctrl = K230_SSI_CTRL_RESET;
+}
+
+static void k230_hi_sys_init(Object *obj)
+{
+    K230HiSysState *s = K230_HI_SYS(obj);
+
+    memory_region_init_io(&s->mmio, obj, &k230_hi_sys_ops, s,
+                          TYPE_K230_HI_SYS, K230_HI_SYS_MMIO_SIZE);
+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio);
+}
+
+static const VMStateDescription vmstate_k230_hi_sys = {
+    .name = TYPE_K230_HI_SYS,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32(ssi_ctrl, K230HiSysState),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void k230_hi_sys_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->vmsd = &vmstate_k230_hi_sys;
+    rc->phases.enter = k230_hi_sys_reset;
+}
+
+static const TypeInfo k230_hi_sys_type_info = {
+    .name = TYPE_K230_HI_SYS,
+    .parent = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(K230HiSysState),
+    .instance_init = k230_hi_sys_init,
+    .class_init = k230_hi_sys_class_init,
+};
+
+static void k230_hi_sys_register_types(void)
+{
+    type_register_static(&k230_hi_sys_type_info);
+}
+
+type_init(k230_hi_sys_register_types)
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 23265f6035..aaae64b33c 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -28,6 +28,7 @@ system_ss.add(when: 'CONFIG_IOSB', if_true: files('iosb.c'))
 system_ss.add(when: 'CONFIG_VIRT_CTRL', if_true: files('virt_ctrl.c'))
 
 # RISC-V devices
+system_ss.add(when: 'CONFIG_K230', if_true: files('k230_hi_sys.c'))
 system_ss.add(when: 'CONFIG_MCHP_PFSOC_DMC', if_true: files('mchp_pfsoc_dmc.c'))
 system_ss.add(when: 'CONFIG_MCHP_PFSOC_IOSCB', if_true: files('mchp_pfsoc_ioscb.c'))
 system_ss.add(when: 'CONFIG_MCHP_PFSOC_SYSREG', if_true: files('mchp_pfsoc_sysreg.c'))
diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
index 36fd1eda22..a666a41c59 100644
--- a/hw/riscv/k230.c
+++ b/hw/riscv/k230.c
@@ -134,6 +134,8 @@ static void k230_soc_init(Object *obj)
                             TYPE_K230_DW_SSI);
     object_initialize_child(obj, "k230-spi-opi", &s->dw_ssi[2],
                             TYPE_K230_DW_SSI);
+    object_initialize_child(obj, "k230-hi-sys", &s->hi_sys,
+                            TYPE_K230_HI_SYS);
 
     qdev_prop_set_uint32(DEVICE(cpu0), "hartid-base", 0);
     qdev_prop_set_string(DEVICE(cpu0), "cpu-type", TYPE_RISCV_CPU_THEAD_C908);
@@ -254,6 +256,18 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
         }
     }
 
+    for (size_t logical_index = 0;
+         logical_index < ARRAY_SIZE(k230_ssi_routes); logical_index++) {
+        const K230SsiRoute *route = &k230_ssi_routes[logical_index];
+
+        k230_hi_sys_set_ssi(&s->hi_sys, logical_index,
+                            &s->dw_ssi[route->ssi_index]);
+    }
+
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->hi_sys), errp)) {
+        return;
+    }
+
     k230_connect_ssi_irqs(s);
 
     sysbus_mmio_map(SYS_BUS_DEVICE(&s->wdt[0]), 0, memmap[K230_DEV_WDT0].base);
@@ -270,6 +284,8 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
                     memmap[K230_DEV_QSPI1].base);
     sysbus_mmio_map(SYS_BUS_DEVICE(&s->dw_ssi[2]), 0,
                     memmap[K230_DEV_SPI].base);
+    sysbus_mmio_map(SYS_BUS_DEVICE(&s->hi_sys), 0,
+                    memmap[K230_DEV_HI_SYS_CFG].base);
 
     /* unimplemented devices */
     create_unimplemented_device("kpu.l2-cache",
@@ -414,9 +430,6 @@ 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("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);
 
diff --git a/hw/ssi/k230_dw_ssi.c b/hw/ssi/k230_dw_ssi.c
index 65b2a8c245..c1c4f40e07 100644
--- a/hw/ssi/k230_dw_ssi.c
+++ b/hw/ssi/k230_dw_ssi.c
@@ -337,6 +337,16 @@ static bool k230_dw_ssi_enabled(const K230DwSsiState *s)
     return FIELD_EX32(s->regs[R_SSIENR], SSIENR, SSIC_EN);
 }
 
+uint32_t k230_dw_ssi_get_spi_mode(const K230DwSsiState *s)
+{
+    return FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, SPI_FRF);
+}
+
+bool k230_dw_ssi_is_sleeping(const K230DwSsiState *s)
+{
+    return s->sleep_status;
+}
+
 static void k230_dw_ssi_deselect(K230DwSsiState *s)
 {
     if (s->active_cs < 0) {
@@ -648,6 +658,7 @@ static bool k230_dw_ssi_idma_triggered(const K230DwSsiState *s)
 static void k230_dw_ssi_idma_end(K230DwSsiState *s, uint32_t cause)
 {
     s->regs[R_SSIENR] = 0;
+    s->sleep_status = true;
     s->phase = K230_DW_SSI_PHASE_IDLE;
     s->remaining_frames = 0;
     k230_dw_ssi_deselect(s);
@@ -1202,9 +1213,11 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
         s->regs[R_SSIENR] = FIELD_DP32(0, SSIENR, SSIC_EN, new_enabled);
         if (!new_enabled) {
             k230_dw_ssi_abort_transfer(s);
+            s->sleep_status = true;
             return;
         }
 
+        s->sleep_status = false;
         k230_dw_ssi_update_cs(s);
         if (k230_dw_ssi_idma_enabled(s)) {
             k230_dw_ssi_try_idma(s);
@@ -1371,6 +1384,7 @@ static void k230_dw_ssi_enter_reset(Object *obj, ResetType type)
     s->irq_latched = 0;
     s->idma_completed_frames = 0;
     memset(&s->enhanced, 0, sizeof(s->enhanced));
+    s->sleep_status = false;
 
     s->regs[R_CTRLR0] = K230_DW_SSI_CTRLR0_RESET;
     s->regs[R_SR] = K230_DW_SSI_SR_RESET;
@@ -1448,6 +1462,7 @@ static const VMStateDescription vmstate_k230_dw_ssi = {
         VMSTATE_UINT32(enhanced.tmod, K230DwSsiState),
         VMSTATE_BOOL(enhanced.mode_bits_enabled, K230DwSsiState),
         VMSTATE_INT32(active_cs, K230DwSsiState),
+        VMSTATE_BOOL(sleep_status, K230DwSsiState),
         VMSTATE_END_OF_LIST()
     },
 };
diff --git a/include/hw/misc/k230_hi_sys.h b/include/hw/misc/k230_hi_sys.h
new file mode 100644
index 0000000000..4c85da531e
--- /dev/null
+++ b/include/hw/misc/k230_hi_sys.h
@@ -0,0 +1,54 @@
+/*
+ * Kendryte K230 HI_SYS
+ *
+ * Copyright (c) 2026 Kangjie Huang <flamboyant.h.01@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Models the HI_SYS system control block, including the SSI_CTRL wrapper
+ * register that K230 firmware uses for XIP enable and SSI mode/sleep
+ * status.
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18):
+ * https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
+ *
+ * For more information, see <https://www.kendryte.com/en/proDetail/230>
+ */
+
+#ifndef HW_MISC_K230_HI_SYS_H
+#define HW_MISC_K230_HI_SYS_H
+
+#include "hw/core/sysbus.h"
+#include "hw/ssi/k230_dw_ssi.h"
+#include "qom/object.h"
+
+#define TYPE_K230_HI_SYS "riscv.k230.hi-sys"
+OBJECT_DECLARE_SIMPLE_TYPE(K230HiSysState, K230_HI_SYS)
+
+#define K230_HI_SYS_MMIO_SIZE            0x400
+#define K230_HI_SYS_SSI_CTRL_OFFSET      0x068
+
+#define K230_SSI_CTRL_RESET              0x00004000U
+#define K230_SSI_CTRL_IMPLEMENTED_MASK   0x0003fff1U
+#define K230_SSI_CTRL_WRITABLE_MASK      0x0003e001U
+
+#define K230_SSI_CTRL_XIP_EN             (1U << 0)
+#define K230_SSI_CTRL_SPI0_SLEEP         (1U << 4)
+#define K230_SSI_CTRL_SPI0_MODE_SHIFT    5
+#define K230_SSI_CTRL_SPI1_SLEEP         (1U << 7)
+#define K230_SSI_CTRL_SPI1_MODE_SHIFT    8
+#define K230_SSI_CTRL_SPI2_SLEEP         (1U << 10)
+#define K230_SSI_CTRL_SPI2_MODE_SHIFT    11
+
+struct K230HiSysState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion mmio;
+    uint32_t ssi_ctrl;
+    K230DwSsiState *ssi[3];
+};
+
+void k230_hi_sys_set_ssi(K230HiSysState *s, unsigned int index,
+                         K230DwSsiState *ssi);
+
+#endif /* HW_MISC_K230_HI_SYS_H */
diff --git a/include/hw/riscv/k230.h b/include/hw/riscv/k230.h
index 493d739aa4..f280c3bebe 100644
--- a/include/hw/riscv/k230.h
+++ b/include/hw/riscv/k230.h
@@ -16,6 +16,7 @@
 #define HW_K230_H
 
 #include "hw/core/boards.h"
+#include "hw/misc/k230_hi_sys.h"
 #include "hw/riscv/riscv_hart.h"
 #include "hw/ssi/k230_dw_ssi.h"
 #include "hw/watchdog/k230_wdt.h"
@@ -35,6 +36,7 @@ typedef struct K230SoCState {
 
     K230WdtState wdt[2];
     K230DwSsiState dw_ssi[3];
+    K230HiSysState hi_sys;
     MemoryRegion sram;
     MemoryRegion bootrom;
 
diff --git a/include/hw/ssi/k230_dw_ssi.h b/include/hw/ssi/k230_dw_ssi.h
index 3a1b2c08cd..49c137d73f 100644
--- a/include/hw/ssi/k230_dw_ssi.h
+++ b/include/hw/ssi/k230_dw_ssi.h
@@ -96,6 +96,10 @@ struct K230DwSsiState {
     uint32_t num_cs;
     uint32_t max_lines;
     int active_cs;
+    bool sleep_status;
 };
 
+uint32_t k230_dw_ssi_get_spi_mode(const K230DwSsiState *s);
+bool k230_dw_ssi_is_sleeping(const K230DwSsiState *s);
+
 #endif /* HW_SSI_K230_DW_SSI_H */
diff --git a/tests/qtest/k230-dw-ssi-test.c b/tests/qtest/k230-dw-ssi-test.c
index ddf5e13145..6bae56b1a8 100644
--- a/tests/qtest/k230-dw-ssi-test.c
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -14,6 +14,8 @@
 #define K230_SPI0_BASE          0x91584000ULL
 #define K230_SPI1_BASE          0x91582000ULL
 #define K230_SPI2_BASE          0x91583000ULL
+#define K230_HI_SYS_BASE        0x91585000ULL
+#define K230_SSI_CTRL_ADDR      (K230_HI_SYS_BASE + 0x68)
 #define K230_PLIC_BASE          0xf00000000ULL
 #define K230_PLIC_PENDING_BASE  0x1000
 #define K230_SSI_CTRLR0          0x000
@@ -53,6 +55,15 @@
 #define K230_SSI_CTRLR0_WRITABLE_MASK   0x01cf7f1fU
 #define K230_SSI_BAUDR_WRITABLE_MASK    0x0000fffeU
 
+#define K230_SSI_CTRL_RESET             0x00004000U
+#define K230_SSI_CTRL_IMPLEMENTED_MASK  0x0003fff1U
+#define K230_SSI_CTRL_WRITABLE_MASK     0x0003e001U
+#define K230_SSI_CTRL_XIP_EN            BIT(0)
+#define K230_SSI_CTRL_SPI0_SLEEP        BIT(4)
+#define K230_SSI_CTRL_SPI0_MODE_SHIFT   5
+#define K230_SSI_CTRL_SPI1_MODE_SHIFT   8
+#define K230_SSI_CTRL_SPI2_MODE_SHIFT   11
+
 #define K230_SSI_CTRLR0_DFS_MASK        0x1fU
 #define K230_SSI_CTRLR0_TMOD_SHIFT      10
 #define K230_SSI_CTRLR0_SRL             BIT(13)
@@ -767,6 +778,54 @@ static void test_idma(void)
     k230_ssi_flash_image_clear(&image);
 }
 
+static void test_hi_sys(void)
+{
+    QTestState *qts = k230_ssi_start();
+    uint32_t ctrlr0;
+    uint32_t expected_modes;
+
+    g_assert_cmphex(qtest_readl(qts, K230_SSI_CTRL_ADDR),
+                    ==, K230_SSI_CTRL_RESET);
+    qtest_writel(qts, K230_SSI_CTRL_ADDR, UINT32_MAX);
+    g_assert_cmphex(qtest_readl(qts, K230_SSI_CTRL_ADDR) &
+                    K230_SSI_CTRL_IMPLEMENTED_MASK,
+                    ==, (K230_SSI_CTRL_RESET &
+                         ~K230_SSI_CTRL_WRITABLE_MASK) |
+                        K230_SSI_CTRL_WRITABLE_MASK);
+    qtest_system_reset(qts);
+
+    ctrlr0 = k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_CTRLR0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_CTRLR0,
+                    ctrlr0 | (K230_SSI_FRF_QUAD <<
+                              K230_SSI_CTRLR0_SPI_FRF_SHIFT));
+    ctrlr0 = k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_CTRLR0);
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_CTRLR0,
+                    ctrlr0 | (1U << K230_SSI_CTRLR0_SPI_FRF_SHIFT));
+    ctrlr0 = k230_ssi_readl(qts, K230_SPI2_BASE, K230_SSI_CTRLR0);
+    k230_ssi_writel(qts, K230_SPI2_BASE, K230_SSI_CTRLR0,
+                    ctrlr0 | (K230_SSI_FRF_OCTAL <<
+                              K230_SSI_CTRLR0_SPI_FRF_SHIFT));
+    expected_modes = (K230_SSI_FRF_QUAD <<
+                      K230_SSI_CTRL_SPI0_MODE_SHIFT) |
+                     (1U << K230_SSI_CTRL_SPI1_MODE_SHIFT) |
+                     (K230_SSI_FRF_OCTAL <<
+                      K230_SSI_CTRL_SPI2_MODE_SHIFT);
+    g_assert_cmphex(qtest_readl(qts, K230_SSI_CTRL_ADDR) &
+                    ((3U << K230_SSI_CTRL_SPI0_MODE_SHIFT) |
+                     (3U << K230_SSI_CTRL_SPI1_MODE_SHIFT) |
+                     (3U << K230_SSI_CTRL_SPI2_MODE_SHIFT)),
+                    ==, expected_modes);
+
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SSIENR, 1);
+    g_assert_cmphex(qtest_readl(qts, K230_SSI_CTRL_ADDR) &
+                    K230_SSI_CTRL_SPI0_SLEEP, ==, 0);
+    k230_ssi_disable(qts, K230_SPI0_BASE);
+    g_assert_cmphex(qtest_readl(qts, K230_SSI_CTRL_ADDR) &
+                    K230_SSI_CTRL_SPI0_SLEEP,
+                    ==, K230_SSI_CTRL_SPI0_SLEEP);
+    qtest_quit(qts);
+}
+
 int main(int argc, char **argv)
 {
     g_test_init(&argc, &argv, NULL);
@@ -780,5 +839,6 @@ int main(int argc, char **argv)
     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);
+    qtest_add_func("/k230-dw-ssi/hi-sys", test_hi_sys);
     return g_test_run();
 }
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 10/11] hw/ssi: Add K230 SSI XIP read window
  2026-07-26 12:28 ` Kangjie Huang
                   ` (9 preceding siblings ...)
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Expose logical spi0's 128 MiB flash window as a second SSI MMIO region.
Gate accesses through HI_SYS XIP_EN and build Standard, Dual, or Quad
SDR read commands from the XIP instruction, address, mode, and
dummy-cycle registers.

PIO and IDMA cover explicit SPI transactions, while K230 firmware also
uses memory-mapped accesses to read spi0 flash. Model the XIP aperture so
this interface is visible to the guest.

Keep the window read-only, end each access with chip select inactive,
and discard stale PIO state before issuing an XIP command.

Cover the XIP gate, write rejection, 24-bit and 32-bit addressing, Quad
mode and dummy cycles, and PIO/XIP sharing in qtest.

Also verify that U-Boot can boot Linux through the XIP read path.

Signed-off-by: Kangjie Huang <flamboyant.h.01@gmail.com>
---
 docs/system/riscv/k230.rst     |   1 +
 hw/misc/k230_hi_sys.c          |   5 +
 hw/riscv/k230.c                |   9 +-
 hw/ssi/k230_dw_ssi.c           | 176 +++++++++++++++++++++++++++++++++
 include/hw/misc/k230_hi_sys.h  |   1 +
 include/hw/ssi/k230_dw_ssi.h   |   6 ++
 tests/qtest/k230-dw-ssi-test.c |  73 ++++++++++++++
 7 files changed, 268 insertions(+), 3 deletions(-)

diff --git a/docs/system/riscv/k230.rst b/docs/system/riscv/k230.rst
index 3067c1b5d7..e92387ad98 100644
--- a/docs/system/riscv/k230.rst
+++ b/docs/system/riscv/k230.rst
@@ -23,6 +23,7 @@ The ``k230`` machine supports the following devices:
 * 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
+* Read-only 128 MiB XIP window for spi0 flash
 
 Boot options
 ------------
diff --git a/hw/misc/k230_hi_sys.c b/hw/misc/k230_hi_sys.c
index 44f5ac61de..1514f28fac 100644
--- a/hw/misc/k230_hi_sys.c
+++ b/hw/misc/k230_hi_sys.c
@@ -115,6 +115,11 @@ void k230_hi_sys_set_ssi(K230HiSysState *s, unsigned int index,
     s->ssi[index] = ssi;
 }
 
+bool k230_hi_sys_xip_enabled(const K230HiSysState *s)
+{
+    return !!(s->ssi_ctrl & K230_SSI_CTRL_XIP_EN);
+}
+
 static void k230_hi_sys_reset(Object *obj, ResetType type)
 {
     K230HiSysState *s = K230_HI_SYS(obj);
diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
index a666a41c59..e8f3ea499e 100644
--- a/hw/riscv/k230.c
+++ b/hw/riscv/k230.c
@@ -262,6 +262,10 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
 
         k230_hi_sys_set_ssi(&s->hi_sys, logical_index,
                             &s->dw_ssi[route->ssi_index]);
+        if (logical_index == 0) {
+            k230_dw_ssi_set_hi_sys(&s->dw_ssi[route->ssi_index],
+                                   &s->hi_sys);
+        }
     }
 
     if (!sysbus_realize(SYS_BUS_DEVICE(&s->hi_sys), errp)) {
@@ -284,6 +288,8 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
                     memmap[K230_DEV_QSPI1].base);
     sysbus_mmio_map(SYS_BUS_DEVICE(&s->dw_ssi[2]), 0,
                     memmap[K230_DEV_SPI].base);
+    sysbus_mmio_map(SYS_BUS_DEVICE(&s->dw_ssi[2]), 1,
+                    memmap[K230_DEV_FLASH].base);
     sysbus_mmio_map(SYS_BUS_DEVICE(&s->hi_sys), 0,
                     memmap[K230_DEV_HI_SYS_CFG].base);
 
@@ -432,9 +438,6 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
 
     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)
diff --git a/hw/ssi/k230_dw_ssi.c b/hw/ssi/k230_dw_ssi.c
index c1c4f40e07..d187e3345a 100644
--- a/hw/ssi/k230_dw_ssi.c
+++ b/hw/ssi/k230_dw_ssi.c
@@ -19,6 +19,7 @@
 #include "hw/core/registerfields.h"
 #include "hw/core/qdev-properties.h"
 #include "hw/core/irq.h"
+#include "hw/misc/k230_hi_sys.h"
 #include "hw/ssi/k230_dw_ssi.h"
 #include "migration/vmstate.h"
 #include "qapi/error.h"
@@ -347,6 +348,11 @@ bool k230_dw_ssi_is_sleeping(const K230DwSsiState *s)
     return s->sleep_status;
 }
 
+void k230_dw_ssi_set_hi_sys(K230DwSsiState *s, K230HiSysState *hi_sys)
+{
+    s->hi_sys = hi_sys;
+}
+
 static void k230_dw_ssi_deselect(K230DwSsiState *s)
 {
     if (s->active_cs < 0) {
@@ -635,6 +641,171 @@ 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_xip_config_supported(K230DwSsiState *s)
+{
+    uint32_t spi_frf = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, SPI_FRF);
+    uint32_t spi_ctrlr0 = s->regs[R_SPI_CTRLR0];
+    uint32_t required_lines;
+
+    switch (spi_frf) {
+    case 0:
+        required_lines = 1;
+        break;
+    case 1:
+        required_lines = 2;
+        break;
+    case 2:
+        required_lines = 4;
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: XIP does not support SPI_FRF=%u\n",
+                      DEVICE(s)->canonical_path, spi_frf);
+        return false;
+    }
+
+    if (required_lines > s->max_lines ||
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, TRANS_TYPE) > 2) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: unsupported XIP line or transfer type\n",
+                      DEVICE(s)->canonical_path);
+        return false;
+    }
+
+    if (FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, SPI_DDR_EN) ||
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, INST_DDR_EN) ||
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, SPI_RXDS_EN) ||
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, SPI_RXDS_SIG_EN)) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: XIP DDR/RXDS mode is unsupported\n",
+                      DEVICE(s)->canonical_path);
+        return false;
+    }
+
+    return true;
+}
+
+static bool k230_dw_ssi_prepare_xip_command(
+    K230DwSsiState *s, hwaddr address, K230DwSsiEnhancedCommand *command)
+{
+    uint32_t spi_ctrlr0 = s->regs[R_SPI_CTRLR0];
+    uint32_t inst_l = FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, INST_L);
+    uint32_t addr_l = FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, ADDR_L);
+
+    command->instruction_bits =
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, XIP_INST_EN) && inst_l ?
+        1U << (inst_l + 1) : 0;
+    command->address_bits = addr_l << 2;
+    if (command->address_bits > 32) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: unsupported XIP address length %u bits\n",
+                      DEVICE(s)->canonical_path, command->address_bits);
+        return false;
+    }
+
+    command->mode_bits_enabled =
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, XIP_MD_BIT_EN);
+    if (command->mode_bits_enabled) {
+        uint32_t mode_length_encoding =
+            FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, XIP_MBL);
+
+        command->mode_bits = 1U << (mode_length_encoding + 1);
+    }
+
+    command->instruction = s->regs[R_XIP_INCR_INST] &
+        (uint32_t)MAKE_64BIT_MASK(0, command->instruction_bits);
+    command->address = (uint32_t)address &
+        (uint32_t)MAKE_64BIT_MASK(0, command->address_bits);
+    command->mode = s->regs[R_XIP_MODE_BITS] &
+        (uint32_t)MAKE_64BIT_MASK(0, command->mode_bits);
+    command->wait_cycles =
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, WAIT_CYCLES);
+    command->spi_frf = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, SPI_FRF);
+    command->trans_type =
+        FIELD_EX32(spi_ctrlr0, SPI_CTRLR0, TRANS_TYPE);
+
+    return true;
+}
+
+static uint64_t k230_dw_ssi_xip_read(void *opaque, hwaddr address,
+                                     unsigned int size)
+{
+    K230DwSsiState *s = K230_DW_SSI(opaque);
+    K230DwSsiEnhancedCommand command = { 0 };
+    uint64_t value = 0;
+    uint32_t dummy_bytes;
+
+    if (!s->hi_sys || !k230_hi_sys_xip_enabled(s->hi_sys)) {
+        return 0;
+    }
+
+    if (!k230_dw_ssi_xip_config_supported(s) ||
+        !k230_dw_ssi_prepare_xip_command(s, address, &command)) {
+        return 0;
+    }
+
+    if (s->active_cs >= 0 || s->phase != K230_DW_SSI_PHASE_IDLE) {
+        k230_dw_ssi_abort_transfer(s);
+    }
+
+    k230_dw_ssi_select(s, 0);
+    if (s->active_cs != 0) {
+        return 0;
+    }
+
+    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);
+    }
+
+    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);
+    }
+
+    for (unsigned int i = 0; i < size; i++) {
+        value |= (uint64_t)(ssi_transfer(s->spi, 0) & 0xff) << (8 * i);
+    }
+
+    k230_dw_ssi_deselect(s);
+    return value;
+}
+
+static void k230_dw_ssi_xip_write(void *opaque, hwaddr address,
+                                  uint64_t value, unsigned int size)
+{
+    K230DwSsiState *s = K230_DW_SSI(opaque);
+
+    qemu_log_mask(LOG_GUEST_ERROR,
+                  "%s: XIP write at 0x%" HWADDR_PRIx " is unsupported\n",
+                  DEVICE(s)->canonical_path, address);
+}
+
+static const MemoryRegionOps k230_dw_ssi_xip_ops = {
+    .read = k230_dw_ssi_xip_read,
+    .write = k230_dw_ssi_xip_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 8,
+        .unaligned = true,
+    },
+    .impl = {
+        .min_access_size = 1,
+        .max_access_size = 8,
+        .unaligned = true,
+    },
+};
+
 static bool k230_dw_ssi_idma_enabled(const K230DwSsiState *s)
 {
     return FIELD_EX32(s->regs[R_DMACR], DMACR, IDMAE);
@@ -1479,6 +1650,11 @@ static void k230_dw_ssi_init(Object *obj)
                           TYPE_K230_DW_SSI, K230_DW_SSI_MMIO_SIZE);
     sysbus_init_mmio(sbd, &s->mmio);
 
+    memory_region_init_io(&s->xip, obj, &k230_dw_ssi_xip_ops, s,
+                          TYPE_K230_DW_SSI ".xip",
+                          K230_DW_SSI_XIP_WINDOW_SIZE);
+    sysbus_init_mmio(sbd, &s->xip);
+
     for (int i = 0; i < K230_DW_SSI_IRQ_COUNT; i++) {
         sysbus_init_irq(sbd, &s->irqs[i]);
     }
diff --git a/include/hw/misc/k230_hi_sys.h b/include/hw/misc/k230_hi_sys.h
index 4c85da531e..5ef5d64543 100644
--- a/include/hw/misc/k230_hi_sys.h
+++ b/include/hw/misc/k230_hi_sys.h
@@ -50,5 +50,6 @@ struct K230HiSysState {
 
 void k230_hi_sys_set_ssi(K230HiSysState *s, unsigned int index,
                          K230DwSsiState *ssi);
+bool k230_hi_sys_xip_enabled(const K230HiSysState *s);
 
 #endif /* HW_MISC_K230_HI_SYS_H */
diff --git a/include/hw/ssi/k230_dw_ssi.h b/include/hw/ssi/k230_dw_ssi.h
index 49c137d73f..892384f8fb 100644
--- a/include/hw/ssi/k230_dw_ssi.h
+++ b/include/hw/ssi/k230_dw_ssi.h
@@ -30,6 +30,9 @@ OBJECT_DECLARE_SIMPLE_TYPE(K230DwSsiState, K230_DW_SSI)
 #define K230_DW_SSI_REGS_SIZE 0x14c
 #define K230_DW_SSI_NUM_REGS \
     (K230_DW_SSI_REGS_SIZE / sizeof(uint32_t))
+#define K230_DW_SSI_XIP_WINDOW_SIZE 0x08000000
+
+typedef struct K230HiSysState K230HiSysState;
 
 /* SSI GPIO output ordering differs from RISR/ISR bit ordering. */
 typedef enum K230DwSsiIrq {
@@ -78,7 +81,9 @@ struct K230DwSsiState {
     SysBusDevice parent_obj;
 
     MemoryRegion mmio;
+    MemoryRegion xip;
     SSIBus *spi;
+    K230HiSysState *hi_sys;
     qemu_irq *cs_lines;
     qemu_irq irqs[K230_DW_SSI_IRQ_COUNT];
 
@@ -101,5 +106,6 @@ struct K230DwSsiState {
 
 uint32_t k230_dw_ssi_get_spi_mode(const K230DwSsiState *s);
 bool k230_dw_ssi_is_sleeping(const K230DwSsiState *s);
+void k230_dw_ssi_set_hi_sys(K230DwSsiState *s, K230HiSysState *hi_sys);
 
 #endif /* HW_SSI_K230_DW_SSI_H */
diff --git a/tests/qtest/k230-dw-ssi-test.c b/tests/qtest/k230-dw-ssi-test.c
index 6bae56b1a8..e69215ff6c 100644
--- a/tests/qtest/k230-dw-ssi-test.c
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -16,6 +16,7 @@
 #define K230_SPI2_BASE          0x91583000ULL
 #define K230_HI_SYS_BASE        0x91585000ULL
 #define K230_SSI_CTRL_ADDR      (K230_HI_SYS_BASE + 0x68)
+#define K230_FLASH_BASE         0xc0000000ULL
 #define K230_PLIC_BASE          0xf00000000ULL
 #define K230_PLIC_PENDING_BASE  0x1000
 #define K230_SSI_CTRLR0          0x000
@@ -83,6 +84,7 @@
 #define K230_SSI_SPI_CTRLR0_XIP_MD_EN     BIT(7)
 #define K230_SSI_SPI_CTRLR0_INST_L_8      (2U << 8)
 #define K230_SSI_SPI_CTRLR0_WAIT(v)       (((v) & 0x1fU) << 11)
+#define K230_SSI_SPI_CTRLR0_XIP_INST_EN   BIT(20)
 #define K230_SSI_SPI_CTRLR0_SPI_DDR_EN    BIT(16)
 #define K230_SSI_SPI_CTRLR0_XIP_MBL_8     (2U << 26)
 
@@ -469,6 +471,24 @@ static void assert_idma_stopped(QTestState *qts, size_t completed)
                      K230_SSI_SR_CMPLTD_DF_SHIFT, ==, completed);
 }
 
+static void enable_xip(QTestState *qts)
+{
+    qtest_writel(qts, K230_SSI_CTRL_ADDR,
+                 K230_SSI_CTRL_RESET | K230_SSI_CTRL_XIP_EN);
+}
+
+static void configure_xip_read(QTestState *qts, uint8_t opcode,
+                               unsigned int address_bits)
+{
+    uint32_t spi_ctrlr0 = K230_SSI_SPI_CTRLR0_TRANS_TYPE(0) |
+                          K230_SSI_SPI_CTRLR0_ADDR_L(address_bits) |
+                          K230_SSI_SPI_CTRLR0_INST_L_8 |
+                          K230_SSI_SPI_CTRLR0_XIP_INST_EN;
+
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_XIP_INCR_INST, opcode);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SPI_CTRLR0, spi_ctrlr0);
+}
+
 static void test_register_contract(void)
 {
     QTestState *qts = k230_ssi_start();
@@ -826,6 +846,58 @@ static void test_hi_sys(void)
     qtest_quit(qts);
 }
 
+static void test_xip_read_window(void)
+{
+    static const uint8_t id_expected[] = { 0xef, 0x40, 0x19 };
+    K230SsiFlashImage image;
+    QTestState *qts = k230_ssi_start_with_flash(&image);
+    uint64_t low_addr = K230_FLASH_BASE + K230_SSI_FLASH_PATTERN_ADDR;
+    uint32_t ctrlr0;
+    uint32_t spi_ctrlr0;
+    uint8_t command = FLASH_CMD_JEDEC;
+    uint8_t id[ARRAY_SIZE(id_expected)];
+
+    g_assert_cmphex(qtest_readl(qts, low_addr), ==, 0);
+    configure_xip_read(qts, FLASH_CMD_READ, 24);
+    enable_xip(qts);
+    g_assert_cmphex(qtest_readl(qts, low_addr), ==, 0xc33c5aa5);
+    qtest_writeb(qts, low_addr, 0);
+    g_assert_cmphex(qtest_readb(qts, low_addr), ==, 0xa5);
+
+    configure_xip_read(qts, FLASH_CMD_READ4, 32);
+    g_assert_cmphex(qtest_readl(qts, K230_FLASH_BASE +
+                                K230_SSI_FLASH_HIGH_ADDR),
+                    ==, 0x74737271);
+
+    ctrlr0 = k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_CTRLR0);
+    ctrlr0 &= ~K230_SSI_CTRLR0_SPI_FRF_MASK;
+    ctrlr0 |= K230_SSI_FRF_QUAD << K230_SSI_CTRLR0_SPI_FRF_SHIFT;
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_CTRLR0, ctrlr0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_XIP_INCR_INST,
+                    FLASH_CMD_QUAD_IO);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_XIP_MODE_BITS, 0xff);
+    spi_ctrlr0 = K230_SSI_SPI_CTRLR0_TRANS_TYPE(1) |
+                 K230_SSI_SPI_CTRLR0_ADDR_L(24) |
+                 K230_SSI_SPI_CTRLR0_INST_L_8 |
+                 K230_SSI_SPI_CTRLR0_XIP_INST_EN |
+                 K230_SSI_SPI_CTRLR0_XIP_MD_EN |
+                 K230_SSI_SPI_CTRLR0_XIP_MBL_8 |
+                 K230_SSI_SPI_CTRLR0_WAIT(4);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SPI_CTRLR0, spi_ctrlr0);
+    g_assert_cmphex(qtest_readl(qts, low_addr), ==, 0xc33c5aa5);
+
+    k230_ssi_configure(qts, K230_SPI0_BASE, K230_SSI_TMOD_TR, 8, 0);
+    configure_xip_read(qts, FLASH_CMD_READ, 24);
+    k230_ssi_enable_cs(qts, K230_SPI0_BASE, BIT(0));
+    g_assert_cmphex(qtest_readb(qts, low_addr), ==, 0xa5);
+    flash_read_transaction(qts, &command, 1, id, sizeof(id));
+    g_assert_cmpmem(id, sizeof(id), id_expected, sizeof(id_expected));
+    g_assert_cmphex(qtest_readb(qts, low_addr), ==, 0xa5);
+
+    qtest_quit(qts);
+    k230_ssi_flash_image_clear(&image);
+}
+
 int main(int argc, char **argv)
 {
     g_test_init(&argc, &argv, NULL);
@@ -840,5 +912,6 @@ int main(int argc, char **argv)
     qtest_add_func("/k230-dw-ssi/qspi-sdr", test_qspi_sdr);
     qtest_add_func("/k230-dw-ssi/idma", test_idma);
     qtest_add_func("/k230-dw-ssi/hi-sys", test_hi_sys);
+    qtest_add_func("/k230-dw-ssi/xip-read-window", test_xip_read_window);
     return g_test_run();
 }
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 11/11] hw/ssi: Add trace events for K230 DesignWare SSI
  2026-07-26 12:28 ` Kangjie Huang
                   ` (10 preceding siblings ...)
  (?)
@ 2026-07-26 12:28 ` Kangjie Huang
  -1 siblings, 0 replies; 13+ messages in thread
From: Kangjie Huang @ 2026-07-26 12:28 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kangjie Huang, qemu-riscv, Chao Liu, Alistair Francis,
	Alistair Francis, Palmer Dabbelt, Weiwei Li,
	Daniel Henrique Barboza, Liu Zhiwei, Pierrick Bouvier,
	Paolo Bonzini, Fabiano Rosas, Laurent Vivier

Add trace events for the K230 DesignWare SSI model covering:

- register accesses using the effective masked or returned value;
- transaction boundaries for standard and enhanced PIO transfers;
- interrupt output levels driven toward the PLIC;
- IDMA start, completion, and AXI error paths; and
- XIP reads with the final returned value.

DR accesses are excluded from register tracing: every FIFO frame
passes through DR, so logging it would drown out everything else
during U-Boot and Linux SPI NOR traffic.

The events avoid tracing individual FIFO data frames so that traces remain
usable during U-Boot and Linux SPI NOR transfers.

The events only observe model behaviour and do not change it.

Signed-off-by: Kangjie Huang <flamboyant.h.01@gmail.com>
---
 hw/ssi/k230_dw_ssi.c | 26 ++++++++++++++++++++++++++
 hw/ssi/trace-events  | 11 +++++++++++
 2 files changed, 37 insertions(+)

diff --git a/hw/ssi/k230_dw_ssi.c b/hw/ssi/k230_dw_ssi.c
index d187e3345a..2bd84a4723 100644
--- a/hw/ssi/k230_dw_ssi.c
+++ b/hw/ssi/k230_dw_ssi.c
@@ -27,6 +27,7 @@
 #include "qemu/log.h"
 #include "qemu/module.h"
 #include "system/dma.h"
+#include "trace.h"
 
 #define K230_DW_SSI_FIFO_CAPACITY 256
 
@@ -285,6 +286,7 @@ 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);
+    trace_k230_dw_ssi_reg_write(s, reg * sizeof(uint32_t), s->regs[reg]);
 }
 
 static uint32_t k230_dw_ssi_irq_raw_status(K230DwSsiState *s)
@@ -312,6 +314,8 @@ static void k230_dw_ssi_update_irq(K230DwSsiState *s)
                       s->regs[R_IMR] & K230_DW_SSI_IRQ_VALID_MASK;
 
     for (int i = 0; i < K230_DW_SSI_IRQ_COUNT; i++) {
+        trace_k230_dw_ssi_irq_update(
+            s, i, !!(status & k230_dw_ssi_irq_status_mask[i]));
         qemu_set_irq(s->irqs[i], !!(status & k230_dw_ssi_irq_status_mask[i]));
     }
 }
@@ -359,6 +363,11 @@ static void k230_dw_ssi_deselect(K230DwSsiState *s)
         return;
     }
 
+    trace_k230_dw_ssi_transaction_end(
+        s, s->active_cs,
+        FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, TMOD),
+        FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, SPI_FRF),
+        fifo32_num_used(&s->tx_fifo), fifo32_num_used(&s->rx_fifo));
     qemu_irq_raise(s->cs_lines[s->active_cs]);
     s->active_cs = -1;
 }
@@ -380,6 +389,10 @@ static void k230_dw_ssi_select(K230DwSsiState *s, unsigned cs)
     k230_dw_ssi_deselect(s);
     qemu_irq_lower(s->cs_lines[cs]);
     s->active_cs = cs;
+    trace_k230_dw_ssi_transaction_start(
+        s, cs, FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, TMOD),
+        FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, SPI_FRF),
+        fifo32_num_used(&s->tx_fifo), fifo32_num_used(&s->rx_fifo));
 }
 
 static void k230_dw_ssi_update_cs(K230DwSsiState *s)
@@ -776,6 +789,7 @@ static uint64_t k230_dw_ssi_xip_read(void *opaque, hwaddr address,
         value |= (uint64_t)(ssi_transfer(s->spi, 0) & 0xff) << (8 * i);
     }
 
+    trace_k230_dw_ssi_xip_read(s, address, size, value);
     k230_dw_ssi_deselect(s);
     return value;
 }
@@ -907,15 +921,19 @@ static void k230_dw_ssi_try_idma(K230DwSsiState *s)
     s->idma_completed_frames = 0;
 
     if (address > UINT64_MAX - (length - 1)) {
+        trace_k230_dw_ssi_idma_error(s, 0, address);
         k230_dw_ssi_idma_fail(s, "address range");
         return;
     }
 
+    trace_k230_dw_ssi_idma_start(
+        s, command.tmod, command.spi_frf, length, address, command.address);
     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) {
+            trace_k230_dw_ssi_idma_error(s, 1, address);
             k230_dw_ssi_idma_fail(s, "source");
             return;
         }
@@ -960,6 +978,7 @@ static void k230_dw_ssi_try_idma(K230DwSsiState *s)
         result = dma_memory_write(&address_space_memory, address, buffer,
                                   length, MEMTXATTRS_UNSPECIFIED);
         if (result != MEMTX_OK) {
+            trace_k230_dw_ssi_idma_error(s, 2, address);
             k230_dw_ssi_idma_fail(s, "destination");
             return;
         }
@@ -970,6 +989,7 @@ static void k230_dw_ssi_try_idma(K230DwSsiState *s)
     }
 
     s->idma_completed_frames = length;
+    trace_k230_dw_ssi_idma_done(s, address, length);
     k230_dw_ssi_idma_end(s, R_RISR_DONER_MASK);
 }
 
@@ -1245,6 +1265,7 @@ static uint64_t k230_dw_ssi_read(void *opaque, hwaddr addr, unsigned int size)
     }
 
     if (k230_dw_ssi_is_razwi(addr)) {
+        trace_k230_dw_ssi_reg_read(s, addr, 0);
         return 0;
     }
 
@@ -1334,6 +1355,7 @@ static uint64_t k230_dw_ssi_read(void *opaque, hwaddr addr, unsigned int size)
         break;
     }
 
+    trace_k230_dw_ssi_reg_read(s, addr, value);
     return value;
 }
 
@@ -1352,6 +1374,7 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
     }
 
     if (k230_dw_ssi_is_razwi(addr)) {
+        trace_k230_dw_ssi_reg_write(s, addr, 0);
         return;
     }
 
@@ -1378,10 +1401,12 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
         bool new_enabled = value & R_SSIENR_SSIC_EN_MASK;
 
         if (old_enabled == new_enabled) {
+            trace_k230_dw_ssi_reg_write(s, addr, s->regs[R_SSIENR]);
             return;
         }
 
         s->regs[R_SSIENR] = FIELD_DP32(0, SSIENR, SSIC_EN, new_enabled);
+        trace_k230_dw_ssi_reg_write(s, addr, s->regs[R_SSIENR]);
         if (!new_enabled) {
             k230_dw_ssi_abort_transfer(s);
             s->sleep_status = true;
@@ -1406,6 +1431,7 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
         uint32_t old_ser = s->regs[R_SER];
 
         s->regs[R_SER] = value & MAKE_64BIT_MASK(0, s->num_cs);
+        trace_k230_dw_ssi_reg_write(s, addr, s->regs[R_SER]);
         if (old_ser && !s->regs[R_SER]) {
             k230_dw_ssi_abort_transfer(s);
             break;
diff --git a/hw/ssi/trace-events b/hw/ssi/trace-events
index b9d8648297..873ca3ba51 100644
--- a/hw/ssi/trace-events
+++ b/hw/ssi/trace-events
@@ -62,3 +62,14 @@ allwinner_a10_spi_tx(uint8_t byte) "write 0x%02x"
 allwinner_a10_spi_rx(uint8_t byte) "read 0x%02x"
 allwinner_a10_spi_read(const char* regname, uint32_t value) "reg[%s] => 0x%08x"
 allwinner_a10_spi_write(const char* regname, uint32_t value) "reg[%s] <= 0x%08x"
+
+# k230_dw_ssi.c
+k230_dw_ssi_reg_read(void *dev, uint64_t addr, uint32_t value) "dev=%p addr=0x%" PRIx64 " value=0x%" PRIx32
+k230_dw_ssi_reg_write(void *dev, uint64_t addr, uint32_t value) "dev=%p addr=0x%" PRIx64 " value=0x%" PRIx32
+k230_dw_ssi_irq_update(void *dev, uint32_t irq, uint32_t level) "dev=%p irq=%" PRIu32 " level=%" PRIu32
+k230_dw_ssi_transaction_start(void *dev, uint32_t cs, uint32_t tmod, uint32_t frf, uint32_t tx, uint32_t rx) "dev=%p cs=%" PRIu32 " tmod=%" PRIu32 " frf=%" PRIu32 " tx=%" PRIu32 " rx=%" PRIu32
+k230_dw_ssi_transaction_end(void *dev, uint32_t cs, uint32_t tmod, uint32_t frf, uint32_t tx, uint32_t rx) "dev=%p cs=%" PRIu32 " tmod=%" PRIu32 " frf=%" PRIu32 " tx=%" PRIu32 " rx=%" PRIu32
+k230_dw_ssi_idma_start(void *dev, uint32_t tmod, uint32_t frf, uint32_t frames, uint64_t guest_addr, uint32_t flash_addr) "dev=%p tmod=%" PRIu32 " frf=%" PRIu32 " frames=%" PRIu32 " guest=0x%" PRIx64 " flash=0x%" PRIx32
+k230_dw_ssi_idma_done(void *dev, uint64_t guest_addr, uint32_t frames) "dev=%p guest=0x%" PRIx64 " frames=%" PRIu32
+k230_dw_ssi_idma_error(void *dev, uint32_t stage, uint64_t guest_addr) "dev=%p stage=%" PRIu32 " guest=0x%" PRIx64
+k230_dw_ssi_xip_read(void *dev, uint64_t addr, uint32_t size, uint64_t value) "dev=%p addr=0x%" PRIx64 " size=%" PRIu32 " value=0x%" PRIx64
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2026-07-26 12:31 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-26 12:27 [PATCH 00/11] hw/riscv: Add K230 SPI, QSPI, IDMA and XIP support Kangjie Huang
2026-07-26 12:28 ` Kangjie Huang
2026-07-26 12:28 ` [PATCH 01/11] hw/ssi: Add K230 DesignWare SSI register model Kangjie Huang
2026-07-26 12:28 ` [PATCH 02/11] hw/riscv/k230: Instantiate K230 SSI controllers Kangjie Huang
2026-07-26 12:28 ` [PATCH 03/11] hw/ssi: Implement K230 SSI FIFO and standard PIO transfers Kangjie Huang
2026-07-26 12:28 ` [PATCH 04/11] hw/ssi: Add K230 SSI interrupt controller Kangjie Huang
2026-07-26 12:28 ` [PATCH 05/11] hw/riscv: Route K230 SSI IRQs to the PLIC Kangjie Huang
2026-07-26 12:28 ` [PATCH 06/11] hw/ssi: Implement K230 enhanced QSPI transfers Kangjie Huang
2026-07-26 12:28 ` [PATCH 07/11] hw/riscv/k230: Attach SPI NOR flash to spi0 Kangjie Huang
2026-07-26 12:28 ` [PATCH 08/11] hw/ssi: Implement K230 SSI internal DMA transfers Kangjie Huang
2026-07-26 12:28 ` [PATCH 09/11] hw/misc: Add K230 HI_SYS SSI control Kangjie Huang
2026-07-26 12:28 ` [PATCH 10/11] hw/ssi: Add K230 SSI XIP read window Kangjie Huang
2026-07-26 12:28 ` [PATCH 11/11] hw/ssi: Add trace events for K230 DesignWare SSI Kangjie Huang

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.