[PATCH v2 1/5] hw/ssi: Add Synopsys DesignWare SSI standard PIO controller
Kangjie Huang <[email protected]> Sun, 2 Aug 2026 03:28:44 +0800
| Newsgroups | org.nongnu.qemu-riscv,org.nongnu.qemu-devel |
|---|---|
| Message-ID | <[email protected]> |
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. + */ + +#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