Re: [PATCH v2 1/5] hw/ssi: Add Synopsys DesignWare SSI standard PIO controller
Chao Liu <[email protected]> Mon, 3 Aug 2026 10:10:18 +0800
| Newsgroups | org.nongnu.qemu-riscv,org.nongnu.qemu-devel |
|---|---|
| Message-ID | <[email protected]> |
On Sun, Aug 02, 2026 at 03:28:44AM +0800, Kangjie Huang wrote: > Add a reusable SysBus model for the Synopsys DesignWare SSI controller. > > Implement the Standard SPI register subset, configurable chip-select and > FIFO resources, the four Standard PIO transfer modes, reset handling, > chip-select GPIOs, and RAZ/WI handling for unsupported enhanced, DMA, > and XIP registers. > > Add num-cs, fifo-depth, and imr-reset properties. Save only Standard PIO > state in VMState and reject migration between devices with different > resource profiles. > > The controller is not instantiated by this patch. > > Signed-off-by: Kangjie Huang <[email protected]> > --- > hw/ssi/Kconfig | 4 + > hw/ssi/dw_ssi.c | 956 ++++++++++++++++++++++++++++++++++++++++ > hw/ssi/meson.build | 1 + > include/hw/ssi/dw_ssi.h | 62 +++ > 4 files changed, 1023 insertions(+) > create mode 100644 hw/ssi/dw_ssi.c > create mode 100644 include/hw/ssi/dw_ssi.h > > diff --git a/hw/ssi/Kconfig b/hw/ssi/Kconfig > index 1bd56463c1..c48cd2ff6d 100644 > --- a/hw/ssi/Kconfig > +++ b/hw/ssi/Kconfig > @@ -6,6 +6,10 @@ config SIFIVE_SPI > bool > select SSI > > +config DW_SSI > + bool > + select SSI > + > config SSI > bool > > diff --git a/hw/ssi/dw_ssi.c b/hw/ssi/dw_ssi.c > new file mode 100644 > index 0000000000..f9f0e23730 > --- /dev/null > +++ b/hw/ssi/dw_ssi.c > @@ -0,0 +1,956 @@ > +/* > + * Synopsys DesignWare SSI > + * > + * Copyright (c) 2026 Kangjie Huang <[email protected]> > + * > + * SPDX-License-Identifier: GPL-2.0-or-later > + * > + * Emulates the DesignWare SSI controller in Standard SPI mode, > + * covering the PIO/FIFO data path and chip selects. Could you share a stable public link to the DesignWare SSI datasheet here? Thanks, Chao > + */ > + > +#include "qemu/osdep.h" > +#include "hw/core/registerfields.h" > +#include "hw/core/qdev-properties.h" > +#include "hw/core/irq.h" > +#include "hw/ssi/dw_ssi.h" > +#include "migration/vmstate.h" > +#include "qapi/error.h" > +#include "qemu/bitops.h" > +#include "qemu/log.h" > +#include "qemu/module.h" > + > +/* DWC SSI 1.03 derivative (TRM v0.3.1 section 12.3). */ > +#define DW_SSI_CTRLR0_RESET 0x00004007 > +#define DW_SSI_SR_RESET 0x00000006 > +#define DW_SSI_IDR_RESET 0xa1b2c3d5 > +#define DW_SSI_VERSION 0x3130332a > +#define DW_SSI_PIO_TX_BATCH 64 > + > +enum { > + DW_SSI_TMOD_TR, > + DW_SSI_TMOD_TO, > + DW_SSI_TMOD_RO, > + DW_SSI_TMOD_EEPROM_READ, > +}; > + > +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 DW_SSI_CTRLR0_STANDARD_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) > + > +#define DW_SSI_CTRLR1_WRITABLE_MASK R_CTRLR1_NDF_MASK > +#define DW_SSI_MWCR_WRITABLE_MASK \ > + (R_MWCR_MWMOD_MASK | R_MWCR_MDD_MASK) > +#define DW_SSI_BAUDR_WRITABLE_MASK R_BAUDR_SCKDV_MASK > +#define DW_SSI_TXFTLR_WRITABLE_MASK \ > + (R_TXFTLR_TFT_MASK | R_TXFTLR_TXFTHR_MASK) > +#define DW_SSI_RXFTLR_WRITABLE_MASK R_RXFTLR_RFT_MASK > +#define 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) > +#define DW_SSI_RX_SAMPLE_DELAY_WRITABLE_MASK \ > + (R_RX_SAMPLE_DELAY_RSD_MASK | R_RX_SAMPLE_DELAY_SE_MASK) > + > +static bool dw_ssi_validate_config(DwSsiState *s, Error **errp) > +{ > + DeviceState *dev = DEVICE(s); > + > + if (s->cfg.num_cs == 0 || s->cfg.num_cs > 8) { > + error_setg(errp, "%s: num-cs must be in range 1..8", > + dev->canonical_path); > + return false; > + } > + > + if (s->cfg.fifo_depth < 2 || s->cfg.fifo_depth > 256) { > + error_setg(errp, "%s: fifo-depth must be in range 2..256", > + dev->canonical_path); > + return false; > + } > + > + if (s->cfg.imr_reset & ~DW_SSI_IMR_WRITABLE_MASK) { > + error_setg(errp, "%s: imr-reset contains unsupported bits", > + dev->canonical_path); > + return false; > + } > + > + return true; > +} > + > +static bool dw_ssi_fifo_threshold_valid(const DwSsiState *s, uint32_t value) > +{ > + return value < s->cfg.fifo_depth; > +} > + > +static void dw_ssi_write_masked(DwSsiState *s, unsigned int reg, > + uint32_t value, uint32_t mask) > +{ > + s->regs[reg] = (s->regs[reg] & ~mask) | (value & mask); > +} > + > +static uint32_t dw_ssi_frame_masked(DwSsiState *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 dw_ssi_is_enabled(const DwSsiState *s) > +{ > + return FIELD_EX32(s->regs[R_SSIENR], SSIENR, SSIC_EN); > +} > + > +static void dw_ssi_deselect(DwSsiState *s) > +{ > + if (s->active_cs < 0) { > + return; > + } > + > + qemu_irq_raise(s->cs_lines[s->active_cs]); > + s->active_cs = -1; > +} > + > +static void dw_ssi_select(DwSsiState *s, unsigned cs) > +{ > + if (cs >= s->cfg.num_cs) { > + qemu_log_mask(LOG_GUEST_ERROR, > + "%s: invalid chip select %u\n", > + DEVICE(s)->canonical_path, cs); > + dw_ssi_deselect(s); > + return; > + } > + > + if (s->active_cs == cs) { > + return; > + } > + > + dw_ssi_deselect(s); > + qemu_irq_lower(s->cs_lines[cs]); > + s->active_cs = cs; > +} > + > +static void dw_ssi_update_cs(DwSsiState *s) > +{ > + uint32_t ser = s->regs[R_SER]; > + > + if (!dw_ssi_is_enabled(s) || !ser) { > + 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); > + dw_ssi_deselect(s); > + return; > + } > + > + dw_ssi_select(s, ctz32(ser)); > +} > + > +static void dw_ssi_abort_transfer(DwSsiState *s) > +{ > + dw_ssi_deselect(s); > + fifo32_reset(&s->tx_fifo); > + fifo32_reset(&s->rx_fifo); > + s->phase = DW_SSI_PHASE_IDLE; > + s->remaining_frames = 0; > +} > + > +static uint32_t dw_ssi_status(DwSsiState *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, BUSY, s->phase != DW_SSI_PHASE_IDLE); > + sr = FIELD_DP32(sr, SR, TFNF, tx_used < s->cfg.fifo_depth); > + 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 == s->cfg.fifo_depth); > + > + return sr; > +} > + > +static void dw_ssi_run_transfer(DwSsiState *s); > + > +static void dw_ssi_push_tx(DwSsiState *s, uint32_t tx) > +{ > + if (!dw_ssi_is_enabled(s)) { > + return; > + } > + > + if (fifo32_is_full(&s->tx_fifo)) { > + return; > + } > + > + fifo32_push(&s->tx_fifo, tx); > + > + if (s->phase != DW_SSI_PHASE_STANDARD_TX_ONLY) { > + dw_ssi_run_transfer(s); > + } > +} > + > +static uint32_t dw_ssi_send_frame(DwSsiState *s, > + uint32_t tx) > +{ > + uint32_t mask = 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 dw_ssi_run_transfer(DwSsiState *s) > +{ > + uint32_t tmod; > + > + if (!dw_ssi_is_enabled(s) || s->active_cs < 0) { > + return; > + } > + > + /* SCKDV=0 disables sclk_out: no transfer can start. */ > + if (FIELD_EX32(s->regs[R_BAUDR], BAUDR, SCKDV) == 0) { > + return; > + } > + > + /* > + * TRM: the controller starts shifting only after the TX FIFO > + * occupancy exceeds TXFTLR.TXFTHR. Transfers already in flight > + * are not interrupted when the FIFO temporarily drops below the > + * level. > + */ > + if (s->phase == DW_SSI_PHASE_IDLE && > + fifo32_num_used(&s->tx_fifo) <= > + FIELD_EX32(s->regs[R_TXFTLR], TXFTLR, TXFTHR)) { > + return; > + } > + > + tmod = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, TMOD); > + > + switch (tmod) { > + case DW_SSI_TMOD_TR: > + if (s->phase == DW_SSI_PHASE_IDLE) { > + s->phase = DW_SSI_PHASE_STANDARD_TR; > + } > + > + while (!fifo32_is_empty(&s->tx_fifo)) { > + uint32_t tx = fifo32_pop(&s->tx_fifo); > + uint32_t rx = dw_ssi_send_frame(s, tx); > + > + if (!fifo32_is_full(&s->rx_fifo)) { > + fifo32_push(&s->rx_fifo, rx); > + } else { > + qemu_log_mask(LOG_GUEST_ERROR, > + "%s: RX FIFO full, dropping frame\n", > + DEVICE(s)->canonical_path); > + break; > + } > + } > + > + if (fifo32_is_empty(&s->tx_fifo)) { > + s->phase = DW_SSI_PHASE_IDLE; > + } > + break; > + case DW_SSI_TMOD_TO: { > + unsigned int frames = 0; > + > + if (fifo32_is_empty(&s->tx_fifo)) { > + s->phase = DW_SSI_PHASE_IDLE; > + break; > + } > + > + s->phase = DW_SSI_PHASE_STANDARD_TX_ONLY; > + while (!fifo32_is_empty(&s->tx_fifo) && > + frames < DW_SSI_PIO_TX_BATCH) { > + uint32_t tx = fifo32_pop(&s->tx_fifo); > + > + dw_ssi_send_frame(s, tx); > + frames++; > + } > + if (fifo32_is_empty(&s->tx_fifo)) { > + s->phase = DW_SSI_PHASE_IDLE; > + } > + break; > + } > + case DW_SSI_TMOD_RO: > + switch (s->phase) { > + case DW_SSI_PHASE_IDLE: > + if (fifo32_is_empty(&s->tx_fifo)) { > + break; > + } > + fifo32_pop(&s->tx_fifo); > + s->phase = DW_SSI_PHASE_RX_ONLY; > + s->remaining_frames = FIELD_EX32(s->regs[R_CTRLR1], CTRLR1, > + NDF) + 1; > + /* Fall through. */ > + case DW_SSI_PHASE_RX_ONLY: > + while (!fifo32_is_full(&s->rx_fifo) && > + s->remaining_frames > 0) { > + uint32_t rx = dw_ssi_send_frame(s, 0x00); > + > + fifo32_push(&s->rx_fifo, rx); > + s->remaining_frames--; > + } > + if (s->remaining_frames == 0) { > + s->phase = DW_SSI_PHASE_IDLE; > + } > + break; > + } > + break; > + case DW_SSI_TMOD_EEPROM_READ: > + switch (s->phase) { > + case DW_SSI_PHASE_IDLE: > + if (fifo32_is_empty(&s->tx_fifo)) { > + break; > + } > + s->phase = DW_SSI_PHASE_EEPROM_COMMAND; > + /* Fall through. */ > + case 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); > + > + dw_ssi_send_frame(s, tx); > + } > + s->phase = DW_SSI_PHASE_EEPROM_DATA; > + s->remaining_frames = FIELD_EX32(s->regs[R_CTRLR1], CTRLR1, > + NDF) + 1; > + /* Fall through. */ > + case DW_SSI_PHASE_EEPROM_DATA: > + while (!fifo32_is_full(&s->rx_fifo) && > + s->remaining_frames > 0) { > + uint32_t rx = dw_ssi_send_frame(s, 0x00); > + > + fifo32_push(&s->rx_fifo, rx); > + s->remaining_frames--; > + } > + if (s->remaining_frames == 0) { > + s->phase = DW_SSI_PHASE_IDLE; > + } > + break; > + } > + break; > + default: > + g_assert_not_reached(); > + } > +} > + > +static bool dw_ssi_is_dr(hwaddr addr) > +{ > + return addr >= A_DR0 && addr <= A_DR_END && > + (addr & 0x3) == 0; > +} > + > +static bool dw_ssi_is_unsupported_offset(hwaddr addr) > +{ > + switch (addr) { > + /* Enhanced SPI extensions. */ > + case A_SPI_CTRLR0: > + case A_DDR_DRIVE_EDGE: > + > + /* XIP extensions. */ > + case A_XIP_MODE_BITS: > + case A_XIP_INCR_INST: > + case A_XIP_WRAP_INST: > + 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: > + > + /* Internal-AXI DMA/IDMA extensions. */ > + case A_DMACR: > + case A_AXIAWLEN: > + case A_AXIARLEN: > + case A_SPIDR: > + case A_SPIAR: > + case A_AXIAR0: > + case A_AXIAR1: > + case A_AXIECR: > + case A_DONECR: > + > + /* SSI unsupported extensions. */ > + case A_RX_SAMPLE_DELAY: > + return true; > + default: > + return false; > + } > +} > + > +static bool dw_ssi_write_requires_disabled(hwaddr addr) > +{ > + switch (addr) { > + case A_CTRLR0: > + case A_CTRLR1: > + case A_MWCR: > + case A_BAUDR: > + return true; > + default: > + return false; > + } > +} > + > +static uint32_t dw_ssi_unsupported_read(DwSsiState *s, > + hwaddr addr) > +{ > + qemu_log_mask(LOG_UNIMP, > + "%s: unsupported read at 0x%" HWADDR_PRIx "\n", > + DEVICE(s)->canonical_path, addr); > + return 0; > +} > + > +static void dw_ssi_unsupported_write(DwSsiState *s, > + hwaddr addr, > + uint32_t value) > +{ > + qemu_log_mask(LOG_UNIMP, > + "%s: unsupported write at 0x%" HWADDR_PRIx > + " value 0x%08x\n", > + DEVICE(s)->canonical_path, addr, value); > +} > + > +static uint64_t dw_ssi_read(void *opaque, hwaddr addr, unsigned int size) > +{ > + DwSsiState *s = DW_SSI(opaque); > + uint32_t value = 0; > + > + if (dw_ssi_is_dr(addr)) { > + if (!fifo32_is_empty(&s->rx_fifo)) { > + value = fifo32_pop(&s->rx_fifo) & dw_ssi_frame_masked(s); > + } else { > + value = 0; > + } > + > + dw_ssi_run_transfer(s); > + return value; > + } > + > + if (dw_ssi_is_unsupported_offset(addr)) { > + value = dw_ssi_unsupported_read(s, addr); > + return value; > + } > + > + 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_IDR: > + case A_SSIC_VERSION_ID: > + value = s->regs[addr / sizeof(uint32_t)]; > + break; > + > + case A_SER: > + value = s->regs[R_SER] & MAKE_64BIT_MASK(0, s->cfg.num_cs); > + break; > + case A_TXFLR: > + value = fifo32_num_used(&s->tx_fifo); > + if (s->phase == DW_SSI_PHASE_STANDARD_TX_ONLY) { > + dw_ssi_run_transfer(s); > + } > + break; > + case A_RXFLR: > + value = fifo32_num_used(&s->rx_fifo); > + break; > + case A_SR: > + value = dw_ssi_status(s); > + if (s->phase == DW_SSI_PHASE_STANDARD_TX_ONLY) { > + dw_ssi_run_transfer(s); > + } > + break; > + default: > + if (addr >= 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 dw_ssi_write(void *opaque, hwaddr addr, > + uint64_t value, unsigned int size) > +{ > + DwSsiState *s = DW_SSI(opaque); > + > + if (dw_ssi_is_dr(addr)) { > + dw_ssi_push_tx(s, value); > + return; > + } > + > + if (dw_ssi_is_unsupported_offset(addr)) { > + dw_ssi_unsupported_write(s, addr, value); > + return; > + } > + > + if (dw_ssi_write_requires_disabled(addr) && > + dw_ssi_is_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: > + value &= ~R_CTRLR0_SPI_FRF_MASK; > + dw_ssi_write_masked(s, R_CTRLR0, value, > + DW_SSI_CTRLR0_STANDARD_WRITABLE_MASK); > + break; > + case A_CTRLR1: > + dw_ssi_write_masked(s, R_CTRLR1, value, > + DW_SSI_CTRLR1_WRITABLE_MASK); > + break; > + case A_SSIENR: { > + bool old_enabled = dw_ssi_is_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) { > + dw_ssi_abort_transfer(s); > + return; > + } > + > + dw_ssi_update_cs(s); > + dw_ssi_run_transfer(s); > + break; > + } > + case A_MWCR: > + dw_ssi_write_masked(s, R_MWCR, value, > + DW_SSI_MWCR_WRITABLE_MASK); > + break; > + case A_SER: { > + uint32_t old_ser = s->regs[R_SER]; > + uint32_t new_ser = value & MAKE_64BIT_MASK(0, s->cfg.num_cs); > + > + /* > + * Changing the selected slave terminates a pending transaction: > + * the FIFOs are flushed only by clearing SSIENR (TRM), but a > + * stale RO/EEPROM phase must not resume on the new chip select. > + */ > + if (old_ser != new_ser && s->phase != DW_SSI_PHASE_IDLE) { > + s->phase = DW_SSI_PHASE_IDLE; > + s->remaining_frames = 0; > + } > + s->regs[R_SER] = new_ser; > + > + dw_ssi_update_cs(s); > + dw_ssi_run_transfer(s); > + break; > + } > + case A_BAUDR: > + dw_ssi_write_masked(s, R_BAUDR, value, > + DW_SSI_BAUDR_WRITABLE_MASK); > + break; > + case A_TXFTLR: > + if (!dw_ssi_fifo_threshold_valid( > + s, FIELD_EX32(value, TXFTLR, TFT)) || > + !dw_ssi_fifo_threshold_valid( > + s, FIELD_EX32(value, TXFTLR, TXFTHR))) { > + qemu_log_mask(LOG_GUEST_ERROR, > + "%s: invalid txftlr value %x\n", > + DEVICE(s)->canonical_path, (unsigned int)value); > + break; > + } > + dw_ssi_write_masked(s, R_TXFTLR, value, > + DW_SSI_TXFTLR_WRITABLE_MASK); > + dw_ssi_run_transfer(s); > + break; > + case A_RXFTLR: > + if (!dw_ssi_fifo_threshold_valid( > + s, FIELD_EX32(value, RXFTLR, RFT))) { > + qemu_log_mask(LOG_GUEST_ERROR, > + "%s: invalid rxftlr value %x\n", > + DEVICE(s)->canonical_path, (unsigned int)value); > + break; > + } > + dw_ssi_write_masked(s, R_RXFTLR, value, > + DW_SSI_RXFTLR_WRITABLE_MASK); > + break; > + case A_TXFLR: > + case A_RXFLR: > + case A_SR: > + break; > + case A_IMR: > + dw_ssi_write_masked(s, R_IMR, value, > + DW_SSI_IMR_WRITABLE_MASK); > + break; > + case A_IDR: > + case A_SSIC_VERSION_ID: > + break; > + default: > + if (addr >= 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 dw_ssi_ops = { > + .read = dw_ssi_read, > + .write = 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 dw_ssi_enter_reset(Object *obj, ResetType type) > +{ > + DwSsiState *s = DW_SSI(obj); > + > + memset(s->regs, 0, sizeof(s->regs)); > + fifo32_reset(&s->tx_fifo); > + fifo32_reset(&s->rx_fifo); > + s->phase = DW_SSI_PHASE_IDLE; > + s->remaining_frames = 0; > + > + s->regs[R_CTRLR0] = DW_SSI_CTRLR0_RESET; > + s->regs[R_SR] = DW_SSI_SR_RESET; > + s->regs[R_IMR] = s->cfg.imr_reset & DW_SSI_IMR_WRITABLE_MASK; > + s->regs[R_IDR] = DW_SSI_IDR_RESET; > + s->regs[R_SSIC_VERSION_ID] = DW_SSI_VERSION; > +} > + > +static void dw_ssi_hold_reset(Object *obj, ResetType type) > +{ > + DwSsiState *s = DW_SSI(obj); > + s->active_cs = -1; > + > + if (s->cs_lines) { > + for (int i = 0; i < s->cfg.num_cs; i++) { > + qemu_irq_raise(s->cs_lines[i]); > + } > + } > +} > + > +static int dw_ssi_post_load(void *opaque, int version_id) > +{ > + DwSsiState *s = opaque; > + uint32_t ndf = FIELD_EX32(s->regs[R_CTRLR1], CTRLR1, NDF) + 1; > + > + if (s->active_cs < -1 || s->active_cs >= (int)s->cfg.num_cs) { > + return -EINVAL; > + } > + if (s->phase > DW_SSI_PHASE_STANDARD_TR) { > + return -EINVAL; > + } > + if (s->phase == DW_SSI_PHASE_RX_ONLY || > + s->phase == DW_SSI_PHASE_EEPROM_DATA) { > + if (s->remaining_frames == 0 || s->remaining_frames > ndf) { > + return -EINVAL; > + } > + } else if (s->remaining_frames != 0) { > + return -EINVAL; > + } > + if (s->active_cs >= 0 && > + (!dw_ssi_is_enabled(s) || > + s->regs[R_SER] != BIT(s->active_cs))) { > + return -EINVAL; > + } > + > + for (int i = 0; i < s->cfg.num_cs; i++) { > + qemu_irq_raise(s->cs_lines[i]); > + } > + if (s->active_cs >= 0) { > + qemu_irq_lower(s->cs_lines[s->active_cs]); > + } > + > + return 0; > +} > + > +static const VMStateDescription vmstate_dw_ssi = { > + .name = TYPE_DW_SSI, > + .version_id = 1, > + .minimum_version_id = 1, > + .post_load = dw_ssi_post_load, > + .fields = (const VMStateField[]) { > + VMSTATE_UINT32_EQUAL(cfg.num_cs, DwSsiState), > + VMSTATE_UINT32_EQUAL(cfg.fifo_depth, DwSsiState), > + VMSTATE_UINT32_EQUAL(cfg.imr_reset, DwSsiState), > + VMSTATE_UINT32_ARRAY(regs, DwSsiState, DW_SSI_NUM_REGS), > + VMSTATE_FIFO32(tx_fifo, DwSsiState), > + VMSTATE_FIFO32(rx_fifo, DwSsiState), > + VMSTATE_UINT32(phase, DwSsiState), > + VMSTATE_UINT32(remaining_frames, DwSsiState), > + VMSTATE_INT32(active_cs, DwSsiState), > + VMSTATE_END_OF_LIST() > + }, > +}; > + > +static void dw_ssi_init(Object *obj) > +{ > + DwSsiState *s = 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, &dw_ssi_ops, s, > + TYPE_DW_SSI, DW_SSI_MMIO_SIZE); > + sysbus_init_mmio(sbd, &s->mmio); > + > + s->active_cs = -1; > +} > + > +static void dw_ssi_realize(DeviceState *dev, Error **errp) > +{ > + DwSsiState *s = DW_SSI(dev); > + > + if (!dw_ssi_validate_config(s, errp)) { > + return; > + } > + > + s->cs_lines = g_new0(qemu_irq, s->cfg.num_cs); > + qdev_init_gpio_out_named(dev, s->cs_lines, "cs", s->cfg.num_cs); > + > + fifo32_create(&s->tx_fifo, s->cfg.fifo_depth); > + fifo32_create(&s->rx_fifo, s->cfg.fifo_depth); > +} > + > +static void dw_ssi_finalize(Object *obj) > +{ > + DwSsiState *s = DW_SSI(obj); > + > + fifo32_destroy(&s->tx_fifo); > + fifo32_destroy(&s->rx_fifo); > + g_free(s->cs_lines); > +} > + > +static const Property dw_ssi_properties[] = { > + DEFINE_PROP_UINT32("num-cs", DwSsiState, cfg.num_cs, 1), > + DEFINE_PROP_UINT32("fifo-depth", DwSsiState, cfg.fifo_depth, 256), > + DEFINE_PROP_UINT32("imr-reset", DwSsiState, > + cfg.imr_reset, 0x0000003f), > +}; > + > +static void dw_ssi_class_init(ObjectClass *klass, const void *data) > +{ > + DeviceClass *dc = DEVICE_CLASS(klass); > + ResettableClass *rc = RESETTABLE_CLASS(klass); > + > + dc->realize = dw_ssi_realize; > + dc->vmsd = &vmstate_dw_ssi; > + device_class_set_props(dc, dw_ssi_properties); > + rc->phases.enter = dw_ssi_enter_reset; > + rc->phases.hold = dw_ssi_hold_reset; > +} > + > +static const TypeInfo dw_ssi_info = { > + .name = TYPE_DW_SSI, > + .parent = TYPE_SYS_BUS_DEVICE, > + .instance_size = sizeof(DwSsiState), > + .instance_init = dw_ssi_init, > + .instance_finalize = dw_ssi_finalize, > + .class_init = dw_ssi_class_init, > +}; > + > +static void dw_ssi_register_types(void) > +{ > + type_register_static(&dw_ssi_info); > +} > + > +type_init(dw_ssi_register_types) > diff --git a/hw/ssi/meson.build b/hw/ssi/meson.build > index 6afb1ea200..447780846c 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_DW_SSI', if_true: files('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/dw_ssi.h b/include/hw/ssi/dw_ssi.h > new file mode 100644 > index 0000000000..a74e906ef8 > --- /dev/null > +++ b/include/hw/ssi/dw_ssi.h > @@ -0,0 +1,62 @@ > +/* > + * Synopsys DesignWare SSI > + * > + * Copyright (c) 2026 Kangjie Huang <[email protected]> > + * > + * SPDX-License-Identifier: GPL-2.0-or-later > + * > + * Emulates the DesignWare SSI controller in Standard SPI mode, > + * covering the PIO/FIFO data path and chip selects. > + */ > + > +#ifndef HW_SSI_DW_SSI_H > +#define HW_SSI_DW_SSI_H > + > +#include "hw/core/sysbus.h" > +#include "hw/ssi/ssi.h" > +#include "qemu/fifo32.h" > +#include "qom/object.h" > + > +#define TYPE_DW_SSI "designware-ssi" > +OBJECT_DECLARE_SIMPLE_TYPE(DwSsiState, DW_SSI) > + > +#define DW_SSI_MMIO_SIZE 0x1000 > +#define DW_SSI_REGS_SIZE 0x14c > +#define DW_SSI_NUM_REGS \ > + (DW_SSI_REGS_SIZE / sizeof(uint32_t)) > + > +typedef enum DwSsiPhase { > + DW_SSI_PHASE_IDLE, > + DW_SSI_PHASE_STANDARD_TX_ONLY, > + DW_SSI_PHASE_RX_ONLY, > + DW_SSI_PHASE_EEPROM_COMMAND, > + DW_SSI_PHASE_EEPROM_DATA, > + DW_SSI_PHASE_STANDARD_TR, > +} DwSsiPhase; > + > +typedef struct DwSsiConfig { > + uint32_t num_cs; > + uint32_t fifo_depth; > + uint32_t imr_reset; > +} DwSsiConfig; > + > +struct DwSsiState { > + SysBusDevice parent_obj; > + > + MemoryRegion mmio; > + SSIBus *spi; > + > + qemu_irq *cs_lines; > + > + Fifo32 tx_fifo; > + Fifo32 rx_fifo; > + uint32_t regs[DW_SSI_NUM_REGS]; > + > + DwSsiConfig cfg; > + > + uint32_t phase; > + uint32_t remaining_frames; > + > + int active_cs; > +}; > +#endif /* HW_SSI_DW_SSI_H */ > -- > 2.43.0 >