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

Reply via email to