Hi Kangjie,
On Sun, Aug 9, 2026 at 2:31 AM Kangjie Huang <[email protected]> wrote:
>
> Implement raw and masked SSI interrupt status, threshold interrupts, and
> the documented read-clear registers.
>
> TXE and RXF are derived from FIFO levels. TXO, RXO, and RXU are latched
> causes. Expose the model's nine physical interrupt outputs.
>
> TXU, DONE, and AXIE depend on transfer engines outside the Standard PIO
> scope and remain deasserted. DONECR and AXIECR are RAZ/WI.
>
> Signed-off-by: Kangjie Huang <[email protected]>
> ---
> hw/ssi/dwc_ssi.c | 129 ++++++++++++++++++++++++++++++++++++++-
> include/hw/ssi/dwc_ssi.h | 18 +++++-
> 2 files changed, 145 insertions(+), 2 deletions(-)
Please squash this patch into patch 1. The introduction of a new model
is normally done in one single commit.
>
> diff --git a/hw/ssi/dwc_ssi.c b/hw/ssi/dwc_ssi.c
> index c0bcf8b869..43c5c5db88 100644
> --- a/hw/ssi/dwc_ssi.c
> +++ b/hw/ssi/dwc_ssi.c
> @@ -6,7 +6,7 @@
> * 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.
> + * covering the PIO/FIFO data path, interrupt outputs and chip selects.
> */
>
> #include "qemu/osdep.h"
> @@ -26,6 +26,9 @@
> #define DWC_SSI_IDR_RESET 0xa1b2c3d5
> #define DWC_SSI_VERSION 0x3130332a
> #define DWC_SSI_PIO_TX_BATCH 64
> +#define DWC_SSI_IRQ_VALID_MASK 0x000009bf
> +#define DWC_SSI_IMPLEMENTED_LATCHED_IRQ_MASK \
> + (R_RISR_TXOIR_MASK | R_RISR_RXOIR_MASK | R_RISR_RXUIR_MASK)
>
> enum {
> DWC_SSI_TMOD_TR,
> @@ -217,6 +220,22 @@ REG32(XIP_WRITE_CTRL, 0x148)
> #define DWC_SSI_RX_SAMPLE_DELAY_WRITABLE_MASK \
> (R_RX_SAMPLE_DELAY_RSD_MASK | R_RX_SAMPLE_DELAY_SE_MASK)
>
> +/*
> + * TXU, DONE and AXIE depend on transfer engines outside Standard PIO.
> + * Their physical outputs are exposed but remain deasserted in this model.
> + */
> +static const uint32_t dwc_ssi_irq_status_mask[DWC_SSI_IRQ_COUNT] = {
> + [DWC_SSI_IRQ_TXE] = R_RISR_TXEIR_MASK,
> + [DWC_SSI_IRQ_TXO] = R_RISR_TXOIR_MASK,
> + [DWC_SSI_IRQ_RXF] = R_RISR_RXFIR_MASK,
> + [DWC_SSI_IRQ_RXO] = R_RISR_RXOIR_MASK,
> + [DWC_SSI_IRQ_TXU] = R_RISR_TXUIR_MASK,
> + [DWC_SSI_IRQ_RXU] = R_RISR_RXUIR_MASK,
> + [DWC_SSI_IRQ_MST] = R_RISR_MSTIR_MASK,
> + [DWC_SSI_IRQ_DONE] = R_RISR_DONER_MASK,
> + [DWC_SSI_IRQ_AXIE] = R_RISR_AXIER_MASK,
> +};
> +
> static bool dwc_ssi_validate_config(DwcSsiState *s, Error **errp)
> {
> DeviceState *dev = DEVICE(s);
> @@ -253,6 +272,45 @@ static void dwc_ssi_write_masked(DwcSsiState *s,
> unsigned int reg,
> s->regs[reg] = (s->regs[reg] & ~mask) | (value & mask);
> }
>
> +static uint32_t dwc_ssi_irq_raw_status(DwcSsiState *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 & DWC_SSI_IRQ_VALID_MASK;
> +}
> +
> +static void dwc_ssi_update_irq(DwcSsiState *s)
> +{
> + uint32_t status = dwc_ssi_irq_raw_status(s) &
> + s->regs[R_IMR] & DWC_SSI_IRQ_VALID_MASK;
> +
> + for (int i = 0; i < DWC_SSI_IRQ_COUNT; i++) {
> + qemu_set_irq(s->irqs[i], !!(status & dwc_ssi_irq_status_mask[i]));
> + }
> +}
> +
> +static uint32_t dwc_ssi_irq_read_clear(DwcSsiState *s,
> + uint32_t clear_mask)
> +{
> + uint32_t active = s->irq_latched & clear_mask;
> +
> + s->irq_latched &= ~clear_mask;
> + dwc_ssi_update_irq(s);
> + return !!active;
> +}
> +
> static uint32_t dwc_ssi_frame_masked(DwcSsiState *s)
> {
> unsigned int bits = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, DFS) + 1;
> @@ -322,6 +380,7 @@ static void dwc_ssi_abort_transfer(DwcSsiState *s)
> s->phase = DWC_SSI_PHASE_IDLE;
> s->remaining_frames = 0;
> s->dummy_frame = 0;
> + dwc_ssi_update_irq(s);
> }
>
> static uint32_t dwc_ssi_status(DwcSsiState *s)
> @@ -349,6 +408,8 @@ static void dwc_ssi_push_tx(DwcSsiState *s, uint32_t tx)
> }
>
> if (fifo32_is_full(&s->tx_fifo)) {
> + s->irq_latched |= R_RISR_TXOIR_MASK;
> + dwc_ssi_update_irq(s);
> return;
> }
>
> @@ -357,6 +418,7 @@ static void dwc_ssi_push_tx(DwcSsiState *s, uint32_t tx)
> if (s->phase != DWC_SSI_PHASE_STANDARD_TX_ONLY) {
> dwc_ssi_run_transfer(s);
> }
> + dwc_ssi_update_irq(s);
> }
>
> static uint32_t dwc_ssi_send_frame(DwcSsiState *s,
> @@ -414,6 +476,7 @@ static void dwc_ssi_run_transfer(DwcSsiState *s)
> uint32_t rx = dwc_ssi_send_frame(s, tx);
>
> if (fifo32_is_full(&s->rx_fifo)) {
> + s->irq_latched |= R_RISR_RXOIR_MASK;
> qemu_log_mask(LOG_GUEST_ERROR,
> "%s: RX FIFO full, dropping frame\n",
> DEVICE(s)->canonical_path);
> @@ -602,9 +665,11 @@ static uint64_t dwc_ssi_read(void *opaque, hwaddr addr,
> unsigned int size)
> value = fifo32_pop(&s->rx_fifo) & dwc_ssi_frame_masked(s);
> } else {
> value = 0;
> + s->irq_latched |= R_RISR_RXUIR_MASK;
> }
>
> dwc_ssi_run_transfer(s);
> + dwc_ssi_update_irq(s);
> return value;
> }
>
> @@ -634,6 +699,7 @@ static uint64_t dwc_ssi_read(void *opaque, hwaddr addr,
> unsigned int size)
> value = fifo32_num_used(&s->tx_fifo);
> if (s->phase == DWC_SSI_PHASE_STANDARD_TX_ONLY) {
> dwc_ssi_run_transfer(s);
> + dwc_ssi_update_irq(s);
> }
> break;
> case A_RXFLR:
> @@ -643,8 +709,35 @@ static uint64_t dwc_ssi_read(void *opaque, hwaddr addr,
> unsigned int size)
> value = dwc_ssi_status(s);
> if (s->phase == DWC_SSI_PHASE_STANDARD_TX_ONLY) {
> dwc_ssi_run_transfer(s);
> + dwc_ssi_update_irq(s);
> }
> break;
> + case A_ISR:
> + value = dwc_ssi_irq_raw_status(s) & s->regs[R_IMR] &
> + DWC_SSI_IRQ_VALID_MASK;
> + break;
> + case A_RISR:
> + value = dwc_ssi_irq_raw_status(s);
> + break;
> + case A_TXEICR:
> + value = dwc_ssi_irq_read_clear(
> + s, R_RISR_TXOIR_MASK | R_RISR_TXUIR_MASK);
> + break;
> + case A_RXOICR:
> + value = dwc_ssi_irq_read_clear(s, R_RISR_RXOIR_MASK);
> + break;
> + case A_RXUICR:
> + value = dwc_ssi_irq_read_clear(s, R_RISR_RXUIR_MASK);
> + break;
> + case A_MSTICR:
> + value = dwc_ssi_irq_read_clear(s, R_RISR_MSTIR_MASK);
> + break;
> + case A_ICR:
> + value = dwc_ssi_irq_read_clear(
> + s, R_RISR_TXOIR_MASK | R_RISR_RXUIR_MASK |
> + R_RISR_RXOIR_MASK | R_RISR_MSTIR_MASK);
> + break;
> +
> default:
> if (addr >= DWC_SSI_REGS_SIZE || (addr & 0x3) != 0) {
> qemu_log_mask(LOG_GUEST_ERROR,
> @@ -708,6 +801,7 @@ static void dwc_ssi_write(void *opaque, hwaddr addr,
>
> dwc_ssi_update_cs(s);
> dwc_ssi_run_transfer(s);
> + dwc_ssi_update_irq(s);
> break;
> }
> case A_MWCR:
> @@ -731,6 +825,7 @@ static void dwc_ssi_write(void *opaque, hwaddr addr,
>
> dwc_ssi_update_cs(s);
> dwc_ssi_run_transfer(s);
> + dwc_ssi_update_irq(s);
> break;
> }
> case A_BAUDR:
> @@ -750,6 +845,7 @@ static void dwc_ssi_write(void *opaque, hwaddr addr,
> dwc_ssi_write_masked(s, R_TXFTLR, value,
> DWC_SSI_TXFTLR_WRITABLE_MASK);
> dwc_ssi_run_transfer(s);
> + dwc_ssi_update_irq(s);
> break;
> case A_RXFTLR:
> if (!dwc_ssi_fifo_threshold_valid(
> @@ -761,6 +857,7 @@ static void dwc_ssi_write(void *opaque, hwaddr addr,
> }
> dwc_ssi_write_masked(s, R_RXFTLR, value,
> DWC_SSI_RXFTLR_WRITABLE_MASK);
> + dwc_ssi_update_irq(s);
> break;
> case A_TXFLR:
> case A_RXFLR:
> @@ -769,6 +866,16 @@ static void dwc_ssi_write(void *opaque, hwaddr addr,
> case A_IMR:
> dwc_ssi_write_masked(s, R_IMR, value,
> DWC_SSI_IMR_WRITABLE_MASK);
> + dwc_ssi_update_irq(s);
> + 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_IDR:
> case A_SSIC_VERSION_ID:
> @@ -809,12 +916,15 @@ static void dwc_ssi_enter_reset(Object *obj, ResetType
> type)
> s->phase = DWC_SSI_PHASE_IDLE;
> s->remaining_frames = 0;
> s->dummy_frame = 0;
> + s->irq_latched = 0;
>
> s->regs[R_CTRLR0] = DWC_SSI_CTRLR0_RESET;
> s->regs[R_SR] = DWC_SSI_SR_RESET;
> s->regs[R_IMR] = s->cfg.imr_reset & DWC_SSI_IMR_WRITABLE_MASK;
> s->regs[R_IDR] = DWC_SSI_IDR_RESET;
> s->regs[R_SSIC_VERSION_ID] = DWC_SSI_VERSION;
> +
> + dwc_ssi_update_irq(s);
> }
>
> static void dwc_ssi_hold_reset(Object *obj, ResetType type)
> @@ -829,6 +939,13 @@ static void dwc_ssi_hold_reset(Object *obj, ResetType
> type)
> }
> }
>
> +static void dwc_ssi_exit_reset(Object *obj, ResetType type)
> +{
> + DwcSsiState *s = DWC_SSI(obj);
> +
> + dwc_ssi_update_irq(s);
> +}
> +
> static bool dwc_ssi_fifo_valid(const Fifo32 *fifo, uint32_t depth)
> {
> const Fifo8 *fifo8 = &fifo->fifo;
> @@ -850,6 +967,9 @@ static int dwc_ssi_post_load(void *opaque, int version_id)
> if (s->active_cs < -1 || s->active_cs >= (int)s->cfg.num_cs) {
> return -EINVAL;
> }
> + if (s->irq_latched & ~DWC_SSI_IMPLEMENTED_LATCHED_IRQ_MASK) {
> + return -EINVAL;
> + }
> if (!dwc_ssi_fifo_valid(&s->tx_fifo, s->cfg.fifo_depth) ||
> !dwc_ssi_fifo_valid(&s->rx_fifo, s->cfg.fifo_depth)) {
> return -EINVAL;
> @@ -878,6 +998,7 @@ static int dwc_ssi_post_load(void *opaque, int version_id)
> qemu_irq_lower(s->cs_lines[s->active_cs]);
> }
>
> + dwc_ssi_update_irq(s);
> return 0;
> }
>
> @@ -893,6 +1014,7 @@ static const VMStateDescription vmstate_dwc_ssi = {
> VMSTATE_UINT32_ARRAY(regs, DwcSsiState, DWC_SSI_NUM_REGS),
> VMSTATE_FIFO32(tx_fifo, DwcSsiState),
> VMSTATE_FIFO32(rx_fifo, DwcSsiState),
> + VMSTATE_UINT32(irq_latched, DwcSsiState),
> VMSTATE_UINT32(phase, DwcSsiState),
> VMSTATE_UINT32(remaining_frames, DwcSsiState),
> VMSTATE_UINT32(dummy_frame, DwcSsiState),
> @@ -913,6 +1035,10 @@ static void dwc_ssi_init(Object *obj)
> TYPE_DWC_SSI, DWC_SSI_MMIO_SIZE);
> sysbus_init_mmio(sbd, &s->mmio);
>
> + for (int i = 0; i < DWC_SSI_IRQ_COUNT; i++) {
> + sysbus_init_irq(sbd, &s->irqs[i]);
> + }
> +
> s->active_cs = -1;
> }
>
> @@ -957,6 +1083,7 @@ static void dwc_ssi_class_init(ObjectClass *klass, const
> void *data)
> device_class_set_props(dc, dwc_ssi_properties);
> rc->phases.enter = dwc_ssi_enter_reset;
> rc->phases.hold = dwc_ssi_hold_reset;
> + rc->phases.exit = dwc_ssi_exit_reset;
> }
>
> static const TypeInfo dwc_ssi_info = {
> diff --git a/include/hw/ssi/dwc_ssi.h b/include/hw/ssi/dwc_ssi.h
> index 6e4f8ea7bb..19499789af 100644
> --- a/include/hw/ssi/dwc_ssi.h
> +++ b/include/hw/ssi/dwc_ssi.h
> @@ -6,7 +6,7 @@
> * 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.
> + * covering the PIO/FIFO data path, interrupt outputs and chip selects.
> */
>
> #ifndef HW_SSI_DWC_SSI_H
> @@ -25,6 +25,20 @@ OBJECT_DECLARE_SIMPLE_TYPE(DwcSsiState, DWC_SSI)
> #define DWC_SSI_NUM_REGS \
> (DWC_SSI_REGS_SIZE / sizeof(uint32_t))
>
> +/* SSI GPIO output ordering differs from RISR/ISR bit ordering. */
> +typedef enum DwcSsiIrq {
> + DWC_SSI_IRQ_TXE,
> + DWC_SSI_IRQ_TXO,
> + DWC_SSI_IRQ_RXF,
> + DWC_SSI_IRQ_RXO,
> + DWC_SSI_IRQ_TXU,
> + DWC_SSI_IRQ_RXU,
> + DWC_SSI_IRQ_MST,
> + DWC_SSI_IRQ_DONE,
> + DWC_SSI_IRQ_AXIE,
> + DWC_SSI_IRQ_COUNT,
> +} DwcSsiIrq;
> +
> typedef enum DwcSsiPhase {
> DWC_SSI_PHASE_IDLE,
> DWC_SSI_PHASE_STANDARD_TX_ONLY,
> @@ -47,6 +61,7 @@ struct DwcSsiState {
> SSIBus *spi;
>
> qemu_irq *cs_lines;
> + qemu_irq irqs[DWC_SSI_IRQ_COUNT];
>
> Fifo32 tx_fifo;
> Fifo32 rx_fifo;
> @@ -54,6 +69,7 @@ struct DwcSsiState {
>
> DwcSsiConfig cfg;
>
> + uint32_t irq_latched;
> uint32_t phase;
> uint32_t remaining_frames;
> uint32_t dummy_frame;
> --
Regards,
Bin