Add the RP2040 XIP/SSI flash model used by the Pico board. The model supports 
direct XIP reads, UF2/ELF/raw overlays from -kernel, raw flash backing files, 
basic NOR program/erase semantics, XIP stream reads and DMA-visible accesses.

The model intentionally stays functional rather than cycle accurate; 
unsupported flash commands are reported through the shared RP2040 
not-yet-implemented diagnostics.

Signed-off-by: Gilles Grimaud <[email protected]>
---
 hw/ssi/Kconfig              |    3 +
 hw/ssi/meson.build          |    1 +
 hw/ssi/rp2040_xip.c         | 1465 +++++++++++++++++++++++++++++++++++
 hw/ssi/trace-events         |   12 +
 include/hw/ssi/rp2040_xip.h |   94 +++
 5 files changed, 1575 insertions(+)
 create mode 100644 hw/ssi/rp2040_xip.c
 create mode 100644 include/hw/ssi/rp2040_xip.h

diff --git a/hw/ssi/Kconfig b/hw/ssi/Kconfig
index 1bd56463c1..e31077862b 100644
--- a/hw/ssi/Kconfig
+++ b/hw/ssi/Kconfig
@@ -29,6 +29,9 @@ config PNV_SPI
     bool
     select SSI
 
+config RP2040_XIP
+    bool
+
 config ALLWINNER_A10_SPI
     bool
     select SSI
diff --git a/hw/ssi/meson.build b/hw/ssi/meson.build
index 6afb1ea200..dc93ed5def 100644
--- a/hw/ssi/meson.build
+++ b/hw/ssi/meson.build
@@ -13,3 +13,4 @@ system_ss.add(when: 'CONFIG_IMX', if_true: files('imx_spi.c'))
 system_ss.add(when: 'CONFIG_IBEX', if_true: files('ibex_spi_host.c'))
 system_ss.add(when: 'CONFIG_BCM2835_SPI', if_true: files('bcm2835_spi.c'))
 system_ss.add(when: 'CONFIG_PNV_SPI', if_true: files('pnv_spi.c'))
+system_ss.add(when: 'CONFIG_RP2040_XIP', if_true: files('rp2040_xip.c'))
diff --git a/hw/ssi/rp2040_xip.c b/hw/ssi/rp2040_xip.c
new file mode 100644
index 0000000000..5cc5ce8093
--- /dev/null
+++ b/hw/ssi/rp2040_xip.c
@@ -0,0 +1,1465 @@
+/*
+ * RP2040 XIP/SSI flash controller emulation
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/units.h"
+#include "qapi/error.h"
+#include "elf.h"
+#include "exec/memattrs.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/core/loader.h"
+#include "hw/misc/rp2040_nyi.h"
+#include "hw/ssi/rp2040_xip.h"
+#include "qemu/log.h"
+#include "system/address-spaces.h"
+#include "trace.h"
+
+#define RP2040_XIP_CTRL_EN           0x1
+#define RP2040_XIP_CTRL_ERR_BADWRITE 0x2
+#define RP2040_XIP_STAT_FLUSH_READY  0x1
+#define RP2040_XIP_STAT_FIFO_EMPTY   0x2
+#define RP2040_XIP_STAT_FIFO_FULL    0x4
+#define RP2040_XIP_FLASH_BASE        0x10000000
+
+#define RP2040_XIP_CTRL              0x00
+#define RP2040_XIP_FLUSH             0x04
+#define RP2040_XIP_STAT              0x08
+#define RP2040_XIP_CTR_HIT           0x0c
+#define RP2040_XIP_CTR_ACC           0x10
+#define RP2040_XIP_STREAM_ADDR       0x14
+#define RP2040_XIP_STREAM_CTR        0x18
+#define RP2040_XIP_STREAM_FIFO       0x1c
+#define RP2040_XIP_STREAM_CTR_MASK   0x003fffff
+
+#define RP2040_SSI_CTRLR0     0x00
+#define RP2040_SSI_CTRLR1     0x04
+#define RP2040_SSI_SSIENR     0x08
+#define RP2040_SSI_SER        0x10
+#define RP2040_SSI_BAUDR      0x14
+#define RP2040_SSI_TXFTLR     0x18
+#define RP2040_SSI_RXFTLR     0x1c
+#define RP2040_SSI_TXFLR      0x20
+#define RP2040_SSI_RXFLR      0x24
+#define RP2040_SSI_SR         0x28
+#define RP2040_SSI_IMR        0x2c
+#define RP2040_SSI_ISR        0x30
+#define RP2040_SSI_RISR       0x34
+#define RP2040_SSI_TXOICR     0x38
+#define RP2040_SSI_RXOICR     0x3c
+#define RP2040_SSI_RXUICR     0x40
+#define RP2040_SSI_MSTICR     0x44
+#define RP2040_SSI_ICR        0x48
+#define RP2040_SSI_DMACR      0x4c
+#define RP2040_SSI_DMATDLR    0x50
+#define RP2040_SSI_DMARDLR    0x54
+#define RP2040_SSI_IDR        0x58
+#define RP2040_SSI_VERSION_ID 0x5c
+#define RP2040_SSI_DR0        0x60
+#define RP2040_SSI_DR_END     0xec
+#define RP2040_SSI_RX_SAMPLE_DLY 0xf0
+#define RP2040_SSI_SPI_CTRLR0 0xf4
+
+#define RP2040_SSI_SR_BUSY 0x01
+#define RP2040_SSI_SR_TFNF 0x02
+#define RP2040_SSI_SR_TFE  0x04
+#define RP2040_SSI_SR_RFNE 0x08
+#define RP2040_SSI_SR_RFF  0x10
+
+#define FLASH_CMD_READ         0x03
+#define FLASH_CMD_WRITE_STATUS 0x01
+#define FLASH_CMD_PAGE_PROGRAM 0x02
+#define FLASH_CMD_READ_STATUS  0x05
+#define FLASH_CMD_READ_STATUS2 0x35
+#define FLASH_CMD_WRITE_ENABLE 0x06
+#define FLASH_CMD_READ_UNIQUE_ID 0x4b
+#define FLASH_CMD_SECTOR_ERASE 0x20
+#define FLASH_CMD_QUAD_IO_READ 0xeb
+#define FLASH_CMD_CONTINUATION_READ 0xa0
+
+#define RP2040_SSI_DMACR_TDMAE BIT(1)
+#define RP2040_SSI_DMACR_RDMAE BIT(0)
+
+#define FLASH_UNIQUE_ID_SIZE 8
+#define FLASH_UNIQUE_ID_DUMMY_BYTES 4
+#define FLASH_UID_DEFAULT 0x3eb8a7493fcc0608ull
+
+#define FLASH_STATUS_WIP 0x01
+#define FLASH_STATUS_WEL 0x02
+#define FLASH_PAGE_SIZE  256
+#define FLASH_SECTOR_SIZE 4096
+
+#define UF2_MAGIC_START0 0x0a324655
+#define UF2_MAGIC_START1 0x9e5d5157
+#define UF2_MAGIC_END    0x0ab16f30
+#define UF2_BLOCK_SIZE   512
+#define UF2_HEADER_SIZE  32
+#define UF2_MAX_PAYLOAD  476
+
+#define UF2_FLAG_NOT_MAIN_FLASH       BIT(0)
+#define UF2_FLAG_FAMILY_ID_PRESENT    BIT(13)
+#define UF2_RP2040_FAMILY_ID          0xe48bff56
+
+#define ATOMIC_ALIAS_MASK 0x3000
+#define ATOMIC_XOR        0x1000
+#define ATOMIC_SET        0x2000
+#define ATOMIC_CLR        0x3000
+
+#define RP2040_BOOT2_SIZE 256
+#define RP2040_BOOT2_CRC_SIZE 252
+#define RP2040_BOOT2_CRC_INIT 0xffffffff
+#define RP2040_BOOT2_CRC_POLY 0x04c11db7
+#define RP2040_SRAM_BASE 0x20000000
+#define RP2040_SRAM_END  0x20042000
+
+static uint32_t rp2040_xip_apply_alias(uint32_t old, uint32_t value,
+                                       hwaddr alias)
+{
+    switch (alias) {
+    case ATOMIC_XOR:
+        return old ^ value;
+    case ATOMIC_SET:
+        return old | value;
+    case ATOMIC_CLR:
+        return old & ~value;
+    default:
+        return value;
+    }
+}
+
+static void rp2040_xip_rx_clear(RP2040XipState *s)
+{
+    s->rx_len = 0;
+    s->rx_pos = 0;
+}
+
+static void rp2040_xip_rx_push(RP2040XipState *s, uint8_t value)
+{
+    if (s->rx_len == ARRAY_SIZE(s->rx) && s->rx_pos > 0) {
+        memmove(s->rx, s->rx + s->rx_pos, s->rx_len - s->rx_pos);
+        s->rx_len -= s->rx_pos;
+        s->rx_pos = 0;
+    }
+
+    if (s->rx_len < ARRAY_SIZE(s->rx)) {
+        s->rx[s->rx_len++] = value;
+        qemu_irq_pulse(s->dreq_rx);
+    }
+}
+
+static bool rp2040_xip_rx_compact(RP2040XipState *s)
+{
+    if (s->rx_len == s->rx_pos) {
+        rp2040_xip_rx_clear(s);
+        return true;
+    }
+    if (s->rx_pos > 0) {
+        memmove(s->rx, s->rx + s->rx_pos, s->rx_len - s->rx_pos);
+        s->rx_len -= s->rx_pos;
+        s->rx_pos = 0;
+        return true;
+    }
+    return false;
+}
+
+static bool rp2040_xip_flash_word(RP2040XipState *s, uint32_t addr,
+                                  uint32_t *value)
+{
+    if (s->flash_size < sizeof(uint32_t) ||
+        addr > s->flash_size - sizeof(uint32_t)) {
+        *value = 0xffffffff;
+        return false;
+    }
+
+    *value = ldl_le_p(s->storage + addr);
+    return true;
+}
+
+static void rp2040_xip_rx_push_ssi_word(RP2040XipState *s, uint32_t value)
+{
+    if (ARRAY_SIZE(s->rx) - (s->rx_len - s->rx_pos) < sizeof(uint32_t)) {
+        rp2040_xip_rx_compact(s);
+    }
+    if (ARRAY_SIZE(s->rx) - s->rx_len < sizeof(uint32_t)) {
+        return;
+    }
+
+    /*
+     * Non-XIP 32-bit SSI reads deliver the serial flash byte stream in the
+     * opposite byte order expected by the RP2040 system bus; SDK users enable
+     * DMA BSWAP when copying words from SSI_DR0.
+     */
+    s->rx[s->rx_len++] = extract32(value, 24, 8);
+    s->rx[s->rx_len++] = extract32(value, 16, 8);
+    s->rx[s->rx_len++] = extract32(value, 8, 8);
+    s->rx[s->rx_len++] = extract32(value, 0, 8);
+    qemu_irq_pulse(s->dreq_rx);
+}
+
+static void rp2040_xip_ssi_bulk_fill(RP2040XipState *s)
+{
+    while (s->ssi_bulk_remaining > 0 &&
+           ARRAY_SIZE(s->rx) - (s->rx_len - s->rx_pos) >=
+           sizeof(uint32_t)) {
+        uint32_t value;
+
+        rp2040_xip_rx_compact(s);
+        rp2040_xip_flash_word(s, s->ssi_bulk_addr, &value);
+        rp2040_xip_rx_push_ssi_word(s, value);
+        s->ssi_bulk_addr += sizeof(uint32_t);
+        s->ssi_bulk_remaining--;
+    }
+}
+
+static void rp2040_xip_ssi_bulk_start(RP2040XipState *s, uint32_t addr,
+                                      uint32_t words)
+{
+    rp2040_xip_rx_clear(s);
+    s->ssi_bulk_addr = addr;
+    s->ssi_bulk_remaining = words;
+    rp2040_xip_ssi_bulk_fill(s);
+}
+
+static void rp2040_xip_update_stream_dreq(RP2040XipState *s)
+{
+    qemu_set_irq(s->dreq_stream, s->stream_fifo_len > s->stream_fifo_pos);
+}
+
+static void rp2040_xip_stream_clear(RP2040XipState *s)
+{
+    s->stream_fifo_len = 0;
+    s->stream_fifo_pos = 0;
+    rp2040_xip_update_stream_dreq(s);
+}
+
+static uint32_t rp2040_xip_stream_word(RP2040XipState *s)
+{
+    uint32_t off;
+
+    if (s->flash_size < sizeof(uint32_t)) {
+        return 0xffffffff;
+    }
+    if (s->stream_addr < RP2040_XIP_FLASH_BASE ||
+        s->stream_addr - RP2040_XIP_FLASH_BASE >
+        s->flash_size - sizeof(uint32_t)) {
+        return 0xffffffff;
+    }
+
+    off = s->stream_addr - RP2040_XIP_FLASH_BASE;
+    return ldl_le_p(s->storage + off);
+}
+
+static void rp2040_xip_stream_fill(RP2040XipState *s)
+{
+    if (s->stream_fifo_pos == s->stream_fifo_len) {
+        s->stream_fifo_pos = 0;
+        s->stream_fifo_len = 0;
+    }
+
+    while (s->stream_ctr > 0 &&
+           s->stream_fifo_len < ARRAY_SIZE(s->stream_fifo)) {
+        s->stream_fifo[s->stream_fifo_len++] = rp2040_xip_stream_word(s);
+        s->stream_addr += 4;
+        s->stream_ctr--;
+    }
+
+    rp2040_xip_update_stream_dreq(s);
+}
+
+static uint32_t rp2040_xip_stream_pop(RP2040XipState *s)
+{
+    uint32_t value = 0;
+
+    rp2040_xip_stream_fill(s);
+    if (s->stream_fifo_pos < s->stream_fifo_len) {
+        value = s->stream_fifo[s->stream_fifo_pos++];
+    }
+    rp2040_xip_stream_fill(s);
+    return value;
+}
+
+static uint32_t rp2040_xip_stat(RP2040XipState *s)
+{
+    uint32_t stat = RP2040_XIP_STAT_FLUSH_READY;
+
+    rp2040_xip_stream_fill(s);
+    if (s->stream_fifo_pos == s->stream_fifo_len) {
+        stat |= RP2040_XIP_STAT_FIFO_EMPTY;
+    }
+    if (s->stream_fifo_len - s->stream_fifo_pos ==
+        ARRAY_SIZE(s->stream_fifo)) {
+        stat |= RP2040_XIP_STAT_FIFO_FULL;
+    }
+
+    return stat;
+}
+
+static uint8_t rp2040_xip_status(RP2040XipState *s)
+{
+    uint8_t status = 0;
+
+    if (s->busy) {
+        status |= FLASH_STATUS_WIP;
+    }
+    if (s->write_enable) {
+        status |= FLASH_STATUS_WEL;
+    }
+
+    return status;
+}
+
+static uint32_t rp2040_xip_tx_addr(RP2040XipState *s)
+{
+    return (uint32_t)s->tx[1] << 16 | s->tx[2] << 8 | s->tx[3];
+}
+
+static uint32_t rp2040_xip_quad_io_addr(RP2040XipState *s)
+{
+    return (uint32_t)s->tx[1] << 16 | s->tx[2] << 8 | s->tx[3];
+}
+
+static uint8_t rp2040_xip_flash_uid_byte(RP2040XipState *s, unsigned index)
+{
+    return extract64(s->flash_uid, (FLASH_UNIQUE_ID_SIZE - 1 - index) * 8, 8);
+}
+
+static void rp2040_xip_finish_busy(RP2040XipState *s)
+{
+    s->busy = false;
+}
+
+static void rp2040_xip_reset_tx(RP2040XipState *s)
+{
+    s->tx_len = 0;
+    s->tx_unsupported_logged = false;
+    s->ssi_bulk_remaining = 0;
+}
+
+static bool rp2040_xip_writeback(RP2040XipState *s, Error **errp)
+{
+    g_autoptr(GError) gerr = NULL;
+
+    if (!s->flash_file || !*s->flash_file) {
+        return true;
+    }
+
+    if (!g_file_set_contents(s->flash_file, (const char *)s->storage,
+                             s->flash_size, &gerr)) {
+        error_setg(errp, "could not write flash file '%s': %s",
+                   s->flash_file, gerr->message);
+        return false;
+    }
+
+    return true;
+}
+
+static void rp2040_xip_writeback_or_warn(RP2040XipState *s)
+{
+    Error *local_err = NULL;
+
+    if (!rp2040_xip_writeback(s, &local_err)) {
+        warn_report_err(local_err);
+    }
+}
+
+static uint32_t rp2040_xip_boot2_crc(const uint8_t *data)
+{
+    uint32_t crc = RP2040_BOOT2_CRC_INIT;
+    int i;
+    int bit;
+
+    for (i = 0; i < RP2040_BOOT2_CRC_SIZE; i++) {
+        crc ^= (uint32_t)data[i] << 24;
+        for (bit = 0; bit < 8; bit++) {
+            if (crc & BIT(31)) {
+                crc = (crc << 1) ^ RP2040_BOOT2_CRC_POLY;
+            } else {
+                crc <<= 1;
+            }
+        }
+    }
+
+    return crc;
+}
+
+static bool rp2040_xip_boot2_empty(const uint8_t *data)
+{
+    int i;
+
+    for (i = 0; i < RP2040_BOOT2_SIZE; i++) {
+        if (data[i] != 0xff) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+static bool rp2040_xip_boot2_looks_like_vector_table(const uint8_t *data)
+{
+    uint32_t initial_sp = ldl_le_p(data);
+    uint32_t reset = ldl_le_p(data + 4);
+
+    return initial_sp >= RP2040_SRAM_BASE &&
+           initial_sp <= RP2040_SRAM_END &&
+           reset >= RP2040_XIP_FLASH_BASE &&
+           reset < RP2040_XIP_FLASH_BASE + MiB &&
+           (reset & 1);
+}
+
+static bool rp2040_xip_fixup_boot2(RP2040XipState *s, const char *filename,
+                                   Error **errp)
+{
+    uint32_t crc;
+
+    if (s->flash_size < RP2040_BOOT2_SIZE) {
+        error_setg(errp, "flash is too small for an RP2040 boot2 block");
+        return false;
+    }
+
+    if (rp2040_xip_boot2_empty(s->storage)) {
+        error_setg(errp, "image '%s' does not contain RP2040 boot2 at "
+                   "0x%08x", filename, RP2040_XIP_FLASH_BASE);
+        return false;
+    }
+
+    if (rp2040_xip_boot2_looks_like_vector_table(s->storage)) {
+        error_setg(errp, "image '%s' starts with an application vector table, "
+                   "not RP2040 boot2; use a Pico SDK UF2 or an ELF with "
+                   "boot2 linked at 0x%08x", filename, RP2040_XIP_FLASH_BASE);
+        return false;
+    }
+
+    crc = rp2040_xip_boot2_crc(s->storage);
+    stl_le_p(s->storage + RP2040_BOOT2_CRC_SIZE, crc);
+
+    return true;
+}
+
+static void rp2040_xip_program(RP2040XipState *s)
+{
+    uint32_t addr;
+    uint32_t page_end;
+    unsigned data_len;
+    unsigned i;
+
+    trace_rp2040_xip_program(s->tx_len >= 4 ? rp2040_xip_tx_addr(s) : 0,
+                             s->tx_len > 4 ? s->tx_len - 4 : 0,
+                             s->write_enable);
+
+    if (!s->write_enable) {
+        return;
+    }
+
+    s->write_enable = false;
+
+    if (s->tx_len <= 4) {
+        return;
+    }
+
+    addr = rp2040_xip_tx_addr(s);
+    if (addr >= s->flash_size) {
+        return;
+    }
+
+    page_end = ROUND_UP(addr + 1, FLASH_PAGE_SIZE);
+    data_len = MIN(s->tx_len - 4, page_end - addr);
+    data_len = MIN(data_len, s->flash_size - addr);
+
+    for (i = 0; i < data_len; i++) {
+        s->storage[addr + i] &= s->tx[4 + i];
+    }
+
+    rp2040_xip_writeback_or_warn(s);
+    s->busy = true;
+}
+
+static void rp2040_xip_erase(RP2040XipState *s)
+{
+    uint32_t addr;
+    uint32_t base;
+
+    trace_rp2040_xip_erase(s->tx_len >= 4 ? rp2040_xip_tx_addr(s) : 0,
+                           s->write_enable);
+
+    if (!s->write_enable) {
+        return;
+    }
+
+    s->write_enable = false;
+
+    if (s->tx_len < 4) {
+        return;
+    }
+
+    addr = rp2040_xip_tx_addr(s);
+    base = QEMU_ALIGN_DOWN(addr, FLASH_SECTOR_SIZE);
+    if (base >= s->flash_size) {
+        return;
+    }
+
+    memset(&s->storage[base], 0xff, MIN(FLASH_SECTOR_SIZE,
+                                       s->flash_size - base));
+    rp2040_xip_writeback_or_warn(s);
+    s->busy = true;
+}
+
+static void rp2040_xip_finish_command(RP2040XipState *s)
+{
+    if (s->tx_len == 0) {
+        return;
+    }
+
+    trace_rp2040_xip_finish_command(s->tx[0], s->tx_len);
+
+    switch (s->tx[0]) {
+    case FLASH_CMD_WRITE_STATUS:
+        s->write_enable = false;
+        break;
+    case FLASH_CMD_PAGE_PROGRAM:
+        rp2040_xip_program(s);
+        break;
+    case FLASH_CMD_SECTOR_ERASE:
+        rp2040_xip_erase(s);
+        break;
+    default:
+        break;
+    }
+
+    rp2040_xip_reset_tx(s);
+}
+
+static void rp2040_xip_dr_write(RP2040XipState *s, uint8_t value)
+{
+    uint32_t addr;
+
+    if (s->tx_len < ARRAY_SIZE(s->tx)) {
+        s->tx[s->tx_len++] = value;
+    }
+
+    switch (s->tx[0]) {
+    case FLASH_CMD_WRITE_STATUS:
+        s->write_enable = false;
+        rp2040_xip_rx_push(s, 0);
+        break;
+    case FLASH_CMD_PAGE_PROGRAM:
+    case FLASH_CMD_SECTOR_ERASE:
+        rp2040_xip_rx_push(s, 0);
+        break;
+    case FLASH_CMD_WRITE_ENABLE:
+        s->write_enable = true;
+        rp2040_xip_rx_push(s, 0);
+        rp2040_xip_reset_tx(s);
+        break;
+    case FLASH_CMD_READ_STATUS:
+        rp2040_xip_rx_push(s, rp2040_xip_status(s));
+        rp2040_xip_finish_busy(s);
+        rp2040_xip_reset_tx(s);
+        break;
+    case FLASH_CMD_READ_STATUS2:
+        rp2040_xip_rx_push(s, 0);
+        rp2040_xip_finish_busy(s);
+        rp2040_xip_reset_tx(s);
+        break;
+    case FLASH_CMD_READ_UNIQUE_ID:
+        if (s->tx_len <= 1 + FLASH_UNIQUE_ID_DUMMY_BYTES) {
+            rp2040_xip_rx_push(s, 0);
+        } else {
+            unsigned index = s->tx_len - 2 - FLASH_UNIQUE_ID_DUMMY_BYTES;
+
+            rp2040_xip_rx_push(s, index < FLASH_UNIQUE_ID_SIZE ?
+                               rp2040_xip_flash_uid_byte(s, index) : 0);
+        }
+        break;
+    case FLASH_CMD_READ:
+        if (s->tx_len <= 4) {
+            rp2040_xip_rx_push(s, 0);
+        } else {
+            addr = rp2040_xip_tx_addr(s) + s->tx_len - 5;
+            rp2040_xip_rx_push(s, addr < s->flash_size ?
+                               s->storage[addr] : 0xff);
+        }
+        break;
+    case FLASH_CMD_QUAD_IO_READ:
+        /*
+         * Minimal 0xeb fast-read support for the RP2040 mask ROM path.
+         * The ROM clocks opcode, 24-bit address and mode/dummy bytes before
+         * consuming data. We do not model bus width or wait-cycle timing here.
+         */
+        if (s->tx_len <= 5) {
+            rp2040_xip_rx_push(s, 0);
+        } else {
+            addr = rp2040_xip_quad_io_addr(s) + s->tx_len - 6;
+            rp2040_xip_rx_push(s, addr < s->flash_size ?
+                               s->storage[addr] : 0xff);
+        }
+        break;
+    case 0x00:
+        rp2040_xip_rx_push(s, 0);
+        break;
+    default:
+        /*
+         * The RP2040 boot ROM performs small full-duplex SSI transactions
+         * while probing the flash path. Even for commands we do not model yet,
+         * a transmitted byte clocks one receive byte back from the bus.
+         */
+        if (!s->tx_unsupported_logged) {
+            g_autofree char *detail = g_strdup_printf("opcode 0x%02x",
+                                                      s->tx[0]);
+
+            rp2040_log_nyi("xip.ssi", "flash command", detail);
+            s->tx_unsupported_logged = true;
+        }
+        rp2040_xip_rx_push(s, 0);
+        break;
+    }
+}
+
+static MemTxResult rp2040_xip_read_common(RP2040XipState *s, hwaddr addr,
+                                          uint64_t *data, unsigned size,
+                                          bool synthetic_vector_overlay)
+{
+    uint64_t value = 0;
+    unsigned i;
+
+    if (s->busy || addr + size > s->flash_size) {
+        return MEMTX_ERROR;
+    }
+
+    for (i = 0; i < size; i++) {
+        hwaddr cur = addr + i;
+        uint8_t byte = s->storage[cur];
+
+        /*
+         * Synthetic ROM CI exit support: let firmware copy a HardFault vector
+         * that points back into ROM without mutating persistent flash storage.
+         */
+        if (synthetic_vector_overlay &&
+            s->synthetic_hardfault_vector_enabled &&
+            cur >= RP2040_BOOT2_SIZE + 0x0c &&
+            cur < RP2040_BOOT2_SIZE + 0x10) {
+            byte = extract32(s->synthetic_hardfault_vector,
+                             (cur - (RP2040_BOOT2_SIZE + 0x0c)) * 8, 8);
+        }
+        value |= (uint64_t)byte << (i * 8);
+    }
+    *data = value;
+    return MEMTX_OK;
+}
+
+static MemTxResult rp2040_xip_read(void *opaque, hwaddr addr, uint64_t *data,
+                                   unsigned size, MemTxAttrs attrs)
+{
+    return rp2040_xip_read_common(opaque, addr, data, size, true);
+}
+
+static MemTxResult rp2040_xip_alias_read(void *opaque, hwaddr addr,
+                                         uint64_t *data, unsigned size,
+                                         MemTxAttrs attrs)
+{
+    return rp2040_xip_read_common(opaque, addr, data, size, false);
+}
+
+static MemTxResult rp2040_xip_write(void *opaque, hwaddr addr, uint64_t data,
+                                    unsigned size, MemTxAttrs attrs)
+{
+    RP2040XipState *s = opaque;
+    unsigned i;
+
+    if (!s->xip_writable) {
+        return MEMTX_ERROR;
+    }
+    if (addr + size > s->flash_size) {
+        return MEMTX_ERROR;
+    }
+
+    for (i = 0; i < size; i++) {
+        s->storage[addr + i] = extract64(data, i * 8, 8);
+    }
+    return MEMTX_OK;
+}
+
+static uint64_t rp2040_xip_ctrl_read(void *opaque, hwaddr addr, unsigned size)
+{
+    RP2040XipState *s = opaque;
+    hwaddr offset = addr & 0xfff;
+    uint64_t value;
+
+    switch (offset) {
+    case RP2040_XIP_CTRL:
+        value = s->xip_ctrl;
+        break;
+    case RP2040_XIP_FLUSH:
+    case RP2040_XIP_CTR_HIT:
+    case RP2040_XIP_CTR_ACC:
+        value = 0;
+        break;
+    case RP2040_XIP_STAT:
+        value = rp2040_xip_stat(s);
+        break;
+    case RP2040_XIP_STREAM_ADDR:
+        value = s->stream_addr;
+        break;
+    case RP2040_XIP_STREAM_CTR:
+        value = s->stream_ctr;
+        break;
+    case RP2040_XIP_STREAM_FIFO:
+        value = rp2040_xip_stream_pop(s);
+        break;
+    default:
+        value = 0;
+        qemu_log_mask(LOG_UNIMP, "rp2040.xip.ctrl: unimplemented read  "
+                      "(size %d, addr 0x%08" HWADDR_PRIx
+                      ", offset 0x%04" HWADDR_PRIx
+                      ") -> 0x%0*" PRIx64 "\n",
+                      size, RP2040_XIP_CTRL_BASE + addr, offset,
+                      size << 1, value);
+        break;
+    }
+
+    return value;
+}
+
+static void rp2040_xip_ctrl_write(void *opaque, hwaddr addr, uint64_t value,
+                                  unsigned size)
+{
+    RP2040XipState *s = opaque;
+    hwaddr alias = addr & ATOMIC_ALIAS_MASK;
+    hwaddr offset = addr & 0xfff;
+    uint32_t new_value;
+
+    switch (offset) {
+    case RP2040_XIP_CTRL:
+        new_value = rp2040_xip_apply_alias(s->xip_ctrl, value, alias);
+        s->xip_ctrl = new_value & (RP2040_XIP_CTRL_EN |
+                                   RP2040_XIP_CTRL_ERR_BADWRITE);
+        break;
+    case RP2040_XIP_FLUSH:
+        rp2040_xip_stream_clear(s);
+        break;
+    case RP2040_XIP_CTR_HIT:
+    case RP2040_XIP_CTR_ACC:
+        break;
+    case RP2040_XIP_STREAM_ADDR:
+        s->stream_addr = value & ~3u;
+        break;
+    case RP2040_XIP_STREAM_CTR:
+        s->stream_ctr = value & RP2040_XIP_STREAM_CTR_MASK;
+        if (s->stream_ctr == 0) {
+            rp2040_xip_stream_clear(s);
+        } else {
+            rp2040_xip_stream_fill(s);
+        }
+        break;
+    default:
+        qemu_log_mask(LOG_UNIMP, "rp2040.xip.ctrl: unimplemented write "
+                      "(size %d, addr 0x%08" HWADDR_PRIx
+                      ", offset 0x%04" HWADDR_PRIx
+                      ", value 0x%0*" PRIx64 ")\n",
+                      size, RP2040_XIP_CTRL_BASE + addr, offset,
+                      size << 1, value);
+        break;
+    }
+}
+
+static uint64_t rp2040_xip_aux_read(void *opaque, hwaddr addr, unsigned size)
+{
+    return rp2040_xip_stream_pop(opaque);
+}
+
+static void rp2040_xip_aux_write(void *opaque, hwaddr addr,
+                                 uint64_t value, unsigned size)
+{
+    rp2040_log_nyi("xip.aux", "write",
+                   "XIP auxiliary stream FIFO is read-only");
+}
+
+static uint64_t rp2040_xip_ssi_read(void *opaque, hwaddr addr, unsigned size)
+{
+    RP2040XipState *s = opaque;
+    hwaddr offset = addr & 0xfff;
+    uint8_t value;
+    uint32_t risr = s->rx_len > s->rx_pos ? 0 : 1;
+    uint64_t ret;
+
+    if (offset >= RP2040_SSI_DR0 && offset <= RP2040_SSI_DR_END) {
+        unsigned i;
+
+        ret = 0;
+        for (i = 0; i < size; i++) {
+            if (s->rx_pos == s->rx_len) {
+                rp2040_xip_rx_clear(s);
+                rp2040_xip_ssi_bulk_fill(s);
+            }
+            if (s->rx_pos < s->rx_len) {
+                value = s->rx[s->rx_pos++];
+            } else {
+                value = 0;
+            }
+            ret |= (uint64_t)value << (i * 8);
+        }
+        if (s->rx_pos == s->rx_len) {
+            rp2040_xip_rx_clear(s);
+            rp2040_xip_ssi_bulk_fill(s);
+        }
+        return ret;
+    }
+
+    switch (offset) {
+    case RP2040_SSI_CTRLR0:
+        ret = s->ctrlr0;
+        break;
+    case RP2040_SSI_CTRLR1:
+        ret = s->ctrlr1;
+        break;
+    case RP2040_SSI_SSIENR:
+        ret = s->ssienr;
+        break;
+    case RP2040_SSI_SER:
+        ret = s->ser;
+        break;
+    case RP2040_SSI_BAUDR:
+        ret = s->baudr;
+        break;
+    case RP2040_SSI_TXFTLR:
+        ret = s->txftlr;
+        break;
+    case RP2040_SSI_RXFTLR:
+        ret = s->rxftlr;
+        break;
+    case RP2040_SSI_TXFLR:
+        ret = 0;
+        break;
+    case RP2040_SSI_RXFLR:
+        ret = s->rx_len - s->rx_pos;
+        break;
+    case RP2040_SSI_SR:
+        ret = RP2040_SSI_SR_TFE | RP2040_SSI_SR_TFNF |
+              (s->busy ? RP2040_SSI_SR_BUSY : 0) |
+              (s->rx_len > s->rx_pos ? RP2040_SSI_SR_RFNE : 0) |
+              (s->rx_len - s->rx_pos == ARRAY_SIZE(s->rx) ?
+               RP2040_SSI_SR_RFF : 0);
+        break;
+    case RP2040_SSI_IMR:
+        ret = s->imr;
+        break;
+    case RP2040_SSI_ISR:
+    case RP2040_SSI_RISR:
+        ret = risr;
+        break;
+    case RP2040_SSI_TXOICR:
+    case RP2040_SSI_RXOICR:
+    case RP2040_SSI_RXUICR:
+    case RP2040_SSI_MSTICR:
+    case RP2040_SSI_ICR:
+        ret = 0;
+        break;
+    case RP2040_SSI_DMACR:
+        ret = s->dmacr;
+        break;
+    case RP2040_SSI_DMATDLR:
+        ret = s->dmatdlr;
+        break;
+    case RP2040_SSI_DMARDLR:
+        ret = s->dmardlr;
+        break;
+    case RP2040_SSI_IDR:
+        ret = 0;
+        break;
+    case RP2040_SSI_VERSION_ID:
+        ret = 0x3430312a;
+        break;
+    case RP2040_SSI_RX_SAMPLE_DLY:
+        ret = s->rx_sample_dly;
+        break;
+    case RP2040_SSI_SPI_CTRLR0:
+        ret = s->spi_ctrlr0;
+        break;
+    default:
+        ret = 0;
+        qemu_log_mask(LOG_UNIMP, "rp2040.xip.ssi: unimplemented read  "
+                      "(size %d, addr 0x%08" HWADDR_PRIx
+                      ", offset 0x%04" HWADDR_PRIx
+                      ") -> 0x%0*" PRIx64 "\n",
+                      size, RP2040_XIP_SSI_BASE + addr, offset,
+                      size << 1, ret);
+        break;
+    }
+
+    return ret;
+}
+
+static void rp2040_xip_ssi_write(void *opaque, hwaddr addr, uint64_t value,
+                                 unsigned size)
+{
+    RP2040XipState *s = opaque;
+    hwaddr alias = addr & ATOMIC_ALIAS_MASK;
+    hwaddr offset = addr & 0xfff;
+    uint32_t old_ssienr = s->ssienr;
+    uint32_t old_ser = s->ser;
+    uint32_t new_value;
+
+    if (offset >= RP2040_SSI_DR0 && offset <= RP2040_SSI_DR_END) {
+        if (s->tx_len < 8 || (s->tx_len & 0x3f) == 0) {
+            trace_rp2040_xip_dr_write(value, size, s->tx_len, s->ctrlr0,
+                                      s->ctrlr1, s->spi_ctrlr0);
+        }
+        if (size == 4 && (value & 0xff) == FLASH_CMD_CONTINUATION_READ) {
+            rp2040_xip_ssi_bulk_start(s, value >> 8, s->ctrlr1 + 1);
+            return;
+        }
+        rp2040_xip_dr_write(s, value & 0xff);
+        return;
+    }
+
+    switch (offset) {
+    case RP2040_SSI_CTRLR0:
+        s->ctrlr0 = rp2040_xip_apply_alias(s->ctrlr0, value, alias);
+        trace_rp2040_xip_ctrlr0(s->ctrlr0);
+        break;
+    case RP2040_SSI_CTRLR1:
+        s->ctrlr1 = rp2040_xip_apply_alias(s->ctrlr1, value, alias);
+        trace_rp2040_xip_ctrlr1(s->ctrlr1);
+        break;
+    case RP2040_SSI_SSIENR:
+        new_value = rp2040_xip_apply_alias(s->ssienr, value, alias);
+        s->ssienr = new_value & 1;
+        trace_rp2040_xip_ssienr(old_ssienr, s->ssienr, s->tx_len);
+        if ((old_ssienr & 1) && !s->ssienr) {
+            rp2040_xip_finish_command(s);
+        }
+        if (!s->ssienr) {
+            rp2040_xip_rx_clear(s);
+            rp2040_xip_reset_tx(s);
+        }
+        break;
+    case RP2040_SSI_SER:
+        new_value = rp2040_xip_apply_alias(s->ser, value, alias);
+        s->ser = new_value & 1;
+        trace_rp2040_xip_ser(old_ser, s->ser, s->tx_len);
+        if ((old_ser & 1) && !s->ser) {
+            rp2040_xip_finish_command(s);
+        }
+        break;
+    case RP2040_SSI_BAUDR:
+        new_value = rp2040_xip_apply_alias(s->baudr, value, alias);
+        s->baudr = new_value & 0xffff;
+        break;
+    case RP2040_SSI_TXFTLR:
+        new_value = rp2040_xip_apply_alias(s->txftlr, value, alias);
+        s->txftlr = new_value & 0xff;
+        break;
+    case RP2040_SSI_RXFTLR:
+        new_value = rp2040_xip_apply_alias(s->rxftlr, value, alias);
+        s->rxftlr = new_value & 0xff;
+        break;
+    case RP2040_SSI_IMR:
+        new_value = rp2040_xip_apply_alias(s->imr, value, alias);
+        s->imr = new_value & 0x3f;
+        break;
+    case RP2040_SSI_DMACR:
+        new_value = rp2040_xip_apply_alias(s->dmacr, value, alias);
+        s->dmacr = new_value & (RP2040_SSI_DMACR_TDMAE |
+                                RP2040_SSI_DMACR_RDMAE);
+        break;
+    case RP2040_SSI_DMATDLR:
+        s->dmatdlr = rp2040_xip_apply_alias(s->dmatdlr, value, alias) & 0xff;
+        break;
+    case RP2040_SSI_DMARDLR:
+        s->dmardlr = rp2040_xip_apply_alias(s->dmardlr, value, alias) & 0xff;
+        break;
+    case RP2040_SSI_RX_SAMPLE_DLY:
+        s->rx_sample_dly = rp2040_xip_apply_alias(s->rx_sample_dly, value,
+                                                  alias) & 0xff;
+        break;
+    case RP2040_SSI_SPI_CTRLR0:
+        s->spi_ctrlr0 = rp2040_xip_apply_alias(s->spi_ctrlr0, value, alias);
+        trace_rp2040_xip_spi_ctrlr0(s->spi_ctrlr0);
+        break;
+    default:
+        qemu_log_mask(LOG_UNIMP, "rp2040.xip.ssi: unimplemented write "
+                      "(size %d, addr 0x%08" HWADDR_PRIx
+                      ", offset 0x%04" HWADDR_PRIx
+                      ", value 0x%0*" PRIx64 ")\n",
+                      size, RP2040_XIP_SSI_BASE + addr, offset,
+                      size << 1, value);
+        break;
+    }
+}
+
+static const MemoryRegionOps rp2040_xip_ops = {
+    .read_with_attrs = rp2040_xip_read,
+    .write_with_attrs = rp2040_xip_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+        .unaligned = true,
+    },
+};
+
+static const MemoryRegionOps rp2040_xip_alias_ops = {
+    .read_with_attrs = rp2040_xip_alias_read,
+    .write_with_attrs = rp2040_xip_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+        .unaligned = true,
+    },
+};
+
+static const MemoryRegionOps rp2040_xip_ctrl_ops = {
+    .read = rp2040_xip_ctrl_read,
+    .write = rp2040_xip_ctrl_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+    },
+};
+
+static const MemoryRegionOps rp2040_xip_ssi_ops = {
+    .read = rp2040_xip_ssi_read,
+    .write = rp2040_xip_ssi_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+        .unaligned = true,
+    },
+};
+
+static const MemoryRegionOps rp2040_xip_aux_ops = {
+    .read = rp2040_xip_aux_read,
+    .write = rp2040_xip_aux_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+    },
+};
+
+void rp2040_xip_set_writable(RP2040XipState *s, bool writable)
+{
+    s->xip_writable = writable;
+}
+
+void rp2040_xip_set_synthetic_hardfault_vector(RP2040XipState *s,
+                                               uint32_t handler)
+{
+    s->synthetic_hardfault_vector = handler;
+    s->synthetic_hardfault_vector_enabled = true;
+}
+
+void rp2040_xip_qspi_cs(RP2040XipState *s, bool high)
+{
+    if (s->qspi_cs_high == high) {
+        return;
+    }
+
+    trace_rp2040_xip_qspi_cs(high, s->tx_len);
+    s->qspi_cs_high = high;
+
+    if (high) {
+        rp2040_xip_finish_command(s);
+    } else {
+        rp2040_xip_rx_clear(s);
+        rp2040_xip_reset_tx(s);
+    }
+}
+
+static bool rp2040_xip_load_elf(RP2040XipState *s, const char *filename,
+                                Error **errp)
+{
+    g_autofree gchar *contents = NULL;
+    gsize len;
+    const Elf32_Ehdr *ehdr;
+    const Elf32_Phdr *phdr;
+    int i;
+
+    if (!g_file_get_contents(filename, &contents, &len, NULL)) {
+        error_setg(errp, "could not load flash image '%s'", filename);
+        return true;
+    }
+
+    if (len < sizeof(*ehdr)) {
+        return false;
+    }
+
+    ehdr = (const Elf32_Ehdr *)contents;
+    if (memcmp(ehdr->e_ident, ELFMAG, SELFMAG) != 0) {
+        return false;
+    }
+    if (ehdr->e_ident[EI_CLASS] != ELFCLASS32 ||
+        ehdr->e_ident[EI_DATA] != ELFDATA2LSB ||
+        le16_to_cpu(ehdr->e_machine) != EM_ARM) {
+        error_setg(errp, "unsupported flash ELF image '%s'", filename);
+        return true;
+    }
+    if (le32_to_cpu(ehdr->e_phoff) > len ||
+        le16_to_cpu(ehdr->e_phentsize) != sizeof(*phdr) ||
+        le16_to_cpu(ehdr->e_phnum) >
+        (len - le32_to_cpu(ehdr->e_phoff)) / sizeof(*phdr)) {
+        error_setg(errp, "invalid flash ELF image '%s'", filename);
+        return true;
+    }
+
+    phdr = (const Elf32_Phdr *)(contents + le32_to_cpu(ehdr->e_phoff));
+    for (i = 0; i < le16_to_cpu(ehdr->e_phnum); i++) {
+        uint32_t paddr = le32_to_cpu(phdr[i].p_paddr);
+        uint32_t filesz = le32_to_cpu(phdr[i].p_filesz);
+        uint32_t memsz = le32_to_cpu(phdr[i].p_memsz);
+        uint32_t off = le32_to_cpu(phdr[i].p_offset);
+        uint32_t xip_off;
+
+        if (le32_to_cpu(phdr[i].p_type) != PT_LOAD) {
+            continue;
+        }
+
+        if (filesz == 0) {
+            continue;
+        }
+
+        if (paddr < RP2040_XIP_FLASH_BASE ||
+            paddr - RP2040_XIP_FLASH_BASE > s->flash_size ||
+            filesz > memsz ||
+            filesz > s->flash_size - (paddr - RP2040_XIP_FLASH_BASE) ||
+            off > len ||
+            filesz > len - off) {
+            error_setg(errp, "flash ELF segment is outside XIP storage");
+            return true;
+        }
+
+        xip_off = paddr - RP2040_XIP_FLASH_BASE;
+        memcpy(s->storage + xip_off, contents + off, filesz);
+    }
+
+    return true;
+}
+
+static bool rp2040_xip_load_uf2(RP2040XipState *s, const char *filename,
+                                Error **errp)
+{
+    g_autofree gchar *contents = NULL;
+    gsize len;
+    unsigned blocks;
+    unsigned i;
+    bool copied = false;
+
+    if (!g_file_get_contents(filename, &contents, &len, NULL)) {
+        error_setg(errp, "could not load flash image '%s'", filename);
+        return true;
+    }
+
+    if (len < UF2_BLOCK_SIZE ||
+        ldl_le_p(contents) != UF2_MAGIC_START0 ||
+        ldl_le_p(contents + 4) != UF2_MAGIC_START1) {
+        return false;
+    }
+
+    if (len % UF2_BLOCK_SIZE != 0) {
+        error_setg(errp, "invalid UF2 image '%s': size is not a multiple "
+                   "of %u bytes", filename, UF2_BLOCK_SIZE);
+        return true;
+    }
+
+    blocks = len / UF2_BLOCK_SIZE;
+    for (i = 0; i < blocks; i++) {
+        const uint8_t *block = (uint8_t *)contents + i * UF2_BLOCK_SIZE;
+        uint32_t flags;
+        uint32_t target;
+        uint32_t payload_size;
+        uint32_t family_id;
+        uint32_t offset;
+
+        if (ldl_le_p(block) != UF2_MAGIC_START0 ||
+            ldl_le_p(block + 4) != UF2_MAGIC_START1 ||
+            ldl_le_p(block + UF2_BLOCK_SIZE - 4) != UF2_MAGIC_END) {
+            error_setg(errp, "invalid UF2 image '%s': bad magic in block %u",
+                       filename, i);
+            return true;
+        }
+
+        flags = ldl_le_p(block + 8);
+        if (flags & UF2_FLAG_NOT_MAIN_FLASH) {
+            continue;
+        }
+
+        if (!(flags & UF2_FLAG_FAMILY_ID_PRESENT)) {
+            error_setg(errp, "invalid RP2040 UF2 image '%s': block %u has "
+                       "no family ID", filename, i);
+            return true;
+        }
+
+        family_id = ldl_le_p(block + 28);
+        if (family_id != UF2_RP2040_FAMILY_ID) {
+            error_setg(errp, "unsupported UF2 family ID 0x%08" PRIx32
+                       " in '%s'", family_id, filename);
+            return true;
+        }
+
+        target = ldl_le_p(block + 12);
+        payload_size = ldl_le_p(block + 16);
+        if (payload_size > UF2_MAX_PAYLOAD ||
+            target < RP2040_XIP_FLASH_BASE ||
+            target - RP2040_XIP_FLASH_BASE > s->flash_size ||
+            payload_size > s->flash_size -
+                           (target - RP2040_XIP_FLASH_BASE)) {
+            error_setg(errp, "UF2 block %u in '%s' is outside XIP flash",
+                       i, filename);
+            return true;
+        }
+
+        offset = target - RP2040_XIP_FLASH_BASE;
+        memcpy(s->storage + offset, block + UF2_HEADER_SIZE, payload_size);
+        copied = true;
+    }
+
+    if (!copied) {
+        error_setg(errp, "UF2 image '%s' contains no RP2040 flash payload",
+                   filename);
+    }
+
+    return true;
+}
+
+void rp2040_xip_load_image(RP2040XipState *s, const char *filename,
+                           Error **errp)
+{
+    Error *local_err = NULL;
+    ssize_t image_size;
+
+    if (!filename) {
+        return;
+    }
+
+    if (rp2040_xip_load_elf(s, filename, &local_err)) {
+        if (local_err) {
+            error_propagate(errp, local_err);
+            return;
+        }
+        if (!rp2040_xip_fixup_boot2(s, filename, errp)) {
+            return;
+        }
+        rp2040_xip_writeback(s, errp);
+        return;
+    }
+
+    if (rp2040_xip_load_uf2(s, filename, &local_err)) {
+        if (local_err) {
+            error_propagate(errp, local_err);
+            return;
+        }
+        if (!rp2040_xip_fixup_boot2(s, filename, errp)) {
+            return;
+        }
+        rp2040_xip_writeback(s, errp);
+        return;
+    }
+
+    image_size = load_image_size(filename, s->storage, s->flash_size);
+    if (image_size < 0) {
+        error_setg(errp, "could not load flash image '%s'", filename);
+        return;
+    }
+    if (!rp2040_xip_fixup_boot2(s, filename, errp)) {
+        return;
+    }
+    rp2040_xip_writeback(s, errp);
+}
+
+bool rp2040_xip_flash_range_erase(RP2040XipState *s, uint32_t flash_offs,
+                                  uint32_t count, uint32_t block_size,
+                                  uint8_t block_cmd, Error **errp)
+{
+    if (!QEMU_IS_ALIGNED(flash_offs, FLASH_SECTOR_SIZE) ||
+        !QEMU_IS_ALIGNED(count, FLASH_SECTOR_SIZE)) {
+        error_setg(errp, "flash erase range is not sector-aligned");
+        return false;
+    }
+    if (flash_offs > s->flash_size || count > s->flash_size - flash_offs) {
+        error_setg(errp, "flash erase range is outside XIP storage");
+        return false;
+    }
+    if (block_size && block_size != 64 * KiB) {
+        g_autofree char *detail = g_strdup_printf("block size %" PRIu32,
+                                                  block_size);
+
+        rp2040_log_nyi("bootrom", "flash_range_erase block size", detail);
+    }
+    if (block_cmd != 0x20 && block_cmd != 0xd8) {
+        g_autofree char *detail = g_strdup_printf("erase command 0x%02x",
+                                                  block_cmd);
+
+        rp2040_log_nyi("bootrom", "flash_range_erase command", detail);
+    }
+
+    memset(s->storage + flash_offs, 0xff, count);
+    return rp2040_xip_writeback(s, errp);
+}
+
+bool rp2040_xip_flash_range_program(RP2040XipState *s, uint32_t flash_offs,
+                                    uint32_t data_addr, uint32_t count,
+                                    Error **errp)
+{
+    g_autofree uint8_t *buf = NULL;
+    uint32_t i;
+
+    if (!QEMU_IS_ALIGNED(flash_offs, FLASH_PAGE_SIZE) ||
+        !QEMU_IS_ALIGNED(count, FLASH_PAGE_SIZE)) {
+        error_setg(errp, "flash program range is not page-aligned");
+        return false;
+    }
+    if (flash_offs > s->flash_size || count > s->flash_size - flash_offs) {
+        error_setg(errp, "flash program range is outside XIP storage");
+        return false;
+    }
+
+    buf = g_malloc(count);
+    if (address_space_read(&address_space_memory, data_addr,
+                           MEMTXATTRS_UNSPECIFIED, buf, count) != MEMTX_OK) {
+        error_setg(errp, "could not read flash program buffer at 0x%08" PRIx32,
+                   data_addr);
+        return false;
+    }
+
+    for (i = 0; i < count; i++) {
+        s->storage[flash_offs + i] &= buf[i];
+    }
+
+    return rp2040_xip_writeback(s, errp);
+}
+
+static void rp2040_xip_realize(DeviceState *dev, Error **errp)
+{
+    RP2040XipState *s = RP2040_XIP(dev);
+    g_autofree gchar *contents = NULL;
+    gsize contents_len = 0;
+
+    if (s->flash_size == 0) {
+        error_setg(errp, "flash-size must be non-zero");
+        return;
+    }
+
+    s->xip_writable = true;
+    s->storage = g_malloc0(s->flash_size);
+    memset(s->storage, 0xff, s->flash_size);
+
+    if (s->flash_file) {
+        if (!g_file_get_contents(s->flash_file, &contents, &contents_len,
+                                 NULL)) {
+            error_setg(errp, "could not load flash file '%s'",
+                       s->flash_file);
+            return;
+        }
+        if (contents_len > s->flash_size) {
+            error_setg(errp, "flash file '%s' is %" G_GSIZE_FORMAT
+                       " bytes, larger than %" G_GSIZE_FORMAT
+                       " byte Pico flash",
+                       s->flash_file, contents_len, (gsize)s->flash_size);
+            return;
+        }
+        memcpy(s->storage, contents, contents_len);
+    }
+
+    memory_region_init_io(&s->xip, OBJECT(dev), &rp2040_xip_ops, s,
+                          "rp2040.xip", s->flash_size);
+    memory_region_init_io(&s->xip_noalloc, OBJECT(dev),
+                          &rp2040_xip_alias_ops, s, "rp2040.xip.noalloc",
+                          s->flash_size);
+    memory_region_init_io(&s->xip_nocache, OBJECT(dev),
+                          &rp2040_xip_alias_ops, s, "rp2040.xip.nocache",
+                          s->flash_size);
+    memory_region_init_io(&s->xip_nocache_noalloc, OBJECT(dev),
+                          &rp2040_xip_alias_ops, s,
+                          "rp2040.xip.nocache-noalloc", s->flash_size);
+    memory_region_init_io(&s->ctrl, OBJECT(dev), &rp2040_xip_ctrl_ops, s,
+                          "rp2040.xip.ctrl", RP2040_XIP_CTRL_SIZE);
+    memory_region_init_io(&s->ssi, OBJECT(dev), &rp2040_xip_ssi_ops, s,
+                          "rp2040.xip.ssi", RP2040_XIP_SSI_SIZE);
+    memory_region_init_io(&s->aux, OBJECT(dev), &rp2040_xip_aux_ops, s,
+                          "rp2040.xip.aux", RP2040_XIP_AUX_SIZE);
+
+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->xip);
+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->ctrl);
+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->ssi);
+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->aux);
+}
+
+static void rp2040_xip_reset(DeviceState *dev)
+{
+    RP2040XipState *s = RP2040_XIP(dev);
+
+    s->xip_ctrl = RP2040_XIP_CTRL_EN | RP2040_XIP_CTRL_ERR_BADWRITE;
+    s->ctrlr0 = 0;
+    s->ctrlr1 = 0;
+    s->ssienr = 0;
+    s->ser = 0;
+    s->baudr = 0;
+    s->txftlr = 0;
+    s->rxftlr = 0;
+    s->imr = 0;
+    s->dmacr = 0;
+    s->dmatdlr = 0;
+    s->dmardlr = 4;
+    s->rx_sample_dly = 0;
+    s->spi_ctrlr0 = 0;
+    s->write_enable = false;
+    s->busy = false;
+    s->qspi_cs_high = true;
+    s->stream_addr = 0;
+    s->stream_ctr = 0;
+    rp2040_xip_stream_clear(s);
+    rp2040_xip_reset_tx(s);
+    rp2040_xip_rx_clear(s);
+}
+
+static void rp2040_xip_finalize(Object *obj)
+{
+    RP2040XipState *s = RP2040_XIP(obj);
+
+    g_free(s->flash_file);
+    g_free(s->storage);
+}
+
+static void rp2040_xip_init(Object *obj)
+{
+    RP2040XipState *s = RP2040_XIP(obj);
+
+    qdev_init_gpio_out_named(DEVICE(obj), &s->dreq_rx, "dreq-rx", 1);
+    qdev_init_gpio_out_named(DEVICE(obj), &s->dreq_stream, "dreq-stream", 1);
+}
+
+static const Property rp2040_xip_properties[] = {
+    DEFINE_PROP_UINT32("flash-size", RP2040XipState, flash_size, 2 * MiB),
+    DEFINE_PROP_STRING("flash-file", RP2040XipState, flash_file),
+    DEFINE_PROP_UINT64("flash-uid", RP2040XipState, flash_uid,
+                       FLASH_UID_DEFAULT),
+};
+
+static void rp2040_xip_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->realize = rp2040_xip_realize;
+    device_class_set_legacy_reset(dc, rp2040_xip_reset);
+    device_class_set_props(dc, rp2040_xip_properties);
+}
+
+static const TypeInfo rp2040_xip_info = {
+    .name          = TYPE_RP2040_XIP,
+    .parent        = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(RP2040XipState),
+    .instance_init = rp2040_xip_init,
+    .instance_finalize = rp2040_xip_finalize,
+    .class_init    = rp2040_xip_class_init,
+};
+
+static void rp2040_xip_register_types(void)
+{
+    type_register_static(&rp2040_xip_info);
+}
+type_init(rp2040_xip_register_types)
diff --git a/hw/ssi/trace-events b/hw/ssi/trace-events
index 2f36cf96b8..e1f8cd75ec 100644
--- a/hw/ssi/trace-events
+++ b/hw/ssi/trace-events
@@ -33,6 +33,18 @@ ibex_spi_host_transfer(uint32_t tx_data, uint32_t rx_data) 
"tx_data: 0x%" PRIx32
 ibex_spi_host_write(uint64_t addr, uint32_t size, uint64_t data) "@0x%" PRIx64 
" size %u: 0x%" PRIx64
 ibex_spi_host_read(uint64_t addr, uint32_t size) "@0x%" PRIx64 " size %u:"
 
+# rp2040_xip.c
+rp2040_xip_finish_command(uint8_t cmd, unsigned tx_len) "cmd 0x%02x tx_len %u"
+rp2040_xip_program(uint32_t addr, unsigned len, bool write_enable) "addr 
0x%06x len %u wel %u"
+rp2040_xip_erase(uint32_t addr, bool write_enable) "addr 0x%06x wel %u"
+rp2040_xip_dr_write(uint64_t value, unsigned size, unsigned tx_len, uint32_t 
ctrlr0, uint32_t ctrlr1, uint32_t spi_ctrlr0) "value 0x%" PRIx64 " size %u 
tx_len %u ctrlr0 0x%08x ctrlr1 0x%08x spi_ctrlr0 0x%08x"
+rp2040_xip_ctrlr0(uint32_t value) "value 0x%08x"
+rp2040_xip_ctrlr1(uint32_t value) "value 0x%08x"
+rp2040_xip_spi_ctrlr0(uint32_t value) "value 0x%08x"
+rp2040_xip_qspi_cs(bool high, unsigned tx_len) "high %u tx_len %u"
+rp2040_xip_ssienr(uint32_t old_value, uint32_t new_value, unsigned tx_len) 
"old 0x%x new 0x%x tx_len %u"
+rp2040_xip_ser(uint32_t old_value, uint32_t new_value, unsigned tx_len) "old 
0x%x new 0x%x tx_len %u"
+
 #pnv_spi.c
 pnv_spi_read(uint64_t addr, uint64_t val) "addr 0x%" PRIx64 " val 0x%" PRIx64
 pnv_spi_write(uint64_t addr, uint64_t val) "addr 0x%" PRIx64 " val 0x%" PRIx64
diff --git a/include/hw/ssi/rp2040_xip.h b/include/hw/ssi/rp2040_xip.h
new file mode 100644
index 0000000000..9081f19d51
--- /dev/null
+++ b/include/hw/ssi/rp2040_xip.h
@@ -0,0 +1,94 @@
+/*
+ * RP2040 XIP/SSI flash controller emulation
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_SSI_RP2040_XIP_H
+#define HW_SSI_RP2040_XIP_H
+
+#include "hw/core/sysbus.h"
+#include "hw/core/irq.h"
+#include "qom/object.h"
+
+#define TYPE_RP2040_XIP "rp2040-xip"
+OBJECT_DECLARE_SIMPLE_TYPE(RP2040XipState, RP2040_XIP)
+
+#define RP2040_XIP_CTRL_BASE 0x14000000
+#define RP2040_XIP_SSI_BASE  0x18000000
+#define RP2040_XIP_AUX_BASE  0x50400000
+#define RP2040_XIP_CTRL_SIZE 0x4000
+#define RP2040_XIP_SSI_SIZE  0x4000
+#define RP2040_XIP_AUX_SIZE  0x4000
+#define RP2040_XIP_STREAM_FIFO_DEPTH 4
+
+struct RP2040XipState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion xip;
+    MemoryRegion xip_noalloc;
+    MemoryRegion xip_nocache;
+    MemoryRegion xip_nocache_noalloc;
+    MemoryRegion ctrl;
+    MemoryRegion ssi;
+    MemoryRegion aux;
+    qemu_irq dreq_rx;
+    qemu_irq dreq_stream;
+
+    uint32_t flash_size;
+    char *flash_file;
+    uint64_t flash_uid;
+    uint8_t *storage;
+    bool xip_writable;
+
+    uint32_t xip_ctrl;
+
+    uint32_t ctrlr0;
+    uint32_t ctrlr1;
+    uint32_t ssienr;
+    uint32_t ser;
+    uint32_t baudr;
+    uint32_t txftlr;
+    uint32_t rxftlr;
+    uint32_t imr;
+    uint32_t dmacr;
+    uint32_t dmatdlr;
+    uint32_t dmardlr;
+    uint32_t rx_sample_dly;
+    uint32_t spi_ctrlr0;
+
+    bool write_enable;
+    bool busy;
+    bool synthetic_hardfault_vector_enabled;
+    uint32_t synthetic_hardfault_vector;
+    bool qspi_cs_high;
+    uint8_t tx[260];
+    unsigned tx_len;
+    bool tx_unsupported_logged;
+    uint8_t rx[16];
+    unsigned rx_len;
+    unsigned rx_pos;
+    uint32_t ssi_bulk_addr;
+    uint32_t ssi_bulk_remaining;
+
+    uint32_t stream_addr;
+    uint32_t stream_ctr;
+    uint32_t stream_fifo[RP2040_XIP_STREAM_FIFO_DEPTH];
+    unsigned stream_fifo_len;
+    unsigned stream_fifo_pos;
+};
+
+void rp2040_xip_set_writable(RP2040XipState *s, bool writable);
+void rp2040_xip_set_synthetic_hardfault_vector(RP2040XipState *s,
+                                               uint32_t handler);
+void rp2040_xip_load_image(RP2040XipState *s, const char *filename,
+                           Error **errp);
+void rp2040_xip_qspi_cs(RP2040XipState *s, bool high);
+bool rp2040_xip_flash_range_erase(RP2040XipState *s, uint32_t flash_offs,
+                                  uint32_t count, uint32_t block_size,
+                                  uint8_t block_cmd, Error **errp);
+bool rp2040_xip_flash_range_program(RP2040XipState *s, uint32_t flash_offs,
+                                    uint32_t data_addr, uint32_t count,
+                                    Error **errp);
+
+#endif
-- 
2.53.0


Reply via email to