[PATCH 07/11] hw/riscv/k230: Attach SPI NOR flash to spi0

Kangjie Huang <[email protected]> Sun, 26 Jul 2026 20:28:25 +0800
Newsgroups org.nongnu.qemu-riscv,org.nongnu.qemu-devel
Message-ID <d42e5d4f870c5ea9b5a6f0aa871242dccbe5c3bb.1785064313.git.flamboyant.h.01@gmail.com>
Add the optional spi-flash machine property and attach the selected
m25p80-compatible device to logical spi0 chip select 0.

Use an MTD backend when supplied and keep the erased-flash default when
no drive is configured.

Cover JEDEC identification, standard read and page program, Quad output
read, and Quad page program in qtest.

Also exercise standard SPI reads and writes from U-Boot and Linux.

Also verify the U-Boot-managed boot path by loading the Linux and OpenSBI
payloads from the attached SPI flash instead of injecting them via the
QEMU command line. The system reaches a Linux shell.

Signed-off-by: Kangjie Huang <[email protected]>
---
 docs/system/riscv/k230.rst     |   1 +
 hw/riscv/Kconfig               |   1 +
 hw/riscv/k230.c                |  66 ++++++++
 include/hw/riscv/k230.h        |   2 +
 tests/qtest/k230-dw-ssi-test.c | 285 +++++++++++++++++++++++++++++++++
 5 files changed, 355 insertions(+)

diff --git a/docs/system/riscv/k230.rst b/docs/system/riscv/k230.rst
index 0358709d2c..0d2617c0f9 100644
--- a/docs/system/riscv/k230.rst
+++ b/docs/system/riscv/k230.rst
@@ -21,6 +21,7 @@ The ``k230`` machine supports the following devices:
 * 2 K230 Watchdog Timer
 * 5 UART
 * 3 K230 SSI controllers for SPI and QSPI
+* Optional SPI NOR flash on spi0 CS0
 
 Boot options
 ------------
diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig
index 69059ae044..4984b0c9f3 100644
--- a/hw/riscv/Kconfig
+++ b/hw/riscv/Kconfig
@@ -163,3 +163,4 @@ config K230
     select UNIMP
     select K230_WDT
     select K230_DW_SSI
+    select SSI_M25P80
diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
index 1863d5ecbb..36fd1eda22 100644
--- a/hw/riscv/k230.c
+++ b/hw/riscv/k230.c
@@ -21,7 +21,9 @@
 #include "system/device_tree.h"
 #include "system/system.h"
 #include "system/memory.h"
+#include "system/blockdev.h"
 #include "target/riscv/cpu.h"
+#include "hw/block/flash.h"
 #include "hw/core/loader.h"
 #include "hw/core/sysbus.h"
 #include "hw/riscv/k230.h"
@@ -31,6 +33,7 @@
 #include "hw/intc/sifive_plic.h"
 #include "hw/char/serial-mm.h"
 #include "hw/misc/unimp.h"
+#include "hw/ssi/ssi.h"
 
 /* Align K230_SDK k230_canmv_defconfig */
 #define K230_DIRECT_OPENSBI_ADDR 0x8000000
@@ -512,6 +515,49 @@ static void k230_firmware_boot(K230MachineState *s, MachineState *machine)
                               memmap[K230_DEV_BOOTROM].size, 0, 0);
 }
 
+static char *k230_machine_get_spi_flash(Object *obj, Error **errp)
+{
+    K230MachineState *s = RISCV_K230_MACHINE(obj);
+
+    return g_strdup(s->spi_flash_model);
+}
+
+static void k230_machine_set_spi_flash(Object *obj, const char *value,
+                                       Error **errp)
+{
+    K230MachineState *s = RISCV_K230_MACHINE(obj);
+
+    g_free(s->spi_flash_model);
+    s->spi_flash_model = g_strdup(value);
+}
+
+static void k230_connect_spi_flash(K230DwSsiState *ssi, unsigned int cs,
+                                   const char *flash_type, DriveInfo *dinfo)
+{
+    ObjectClass *flash_class;
+    DeviceState *flash;
+    qemu_irq flash_cs;
+
+    flash_class = module_object_class_by_name(flash_type);
+    if (!flash_class || object_class_is_abstract(flash_class) ||
+        !object_class_dynamic_cast(flash_class, TYPE_M25P80)) {
+        error_report("'%s' is either abstract or not a subtype of m25p80",
+                     flash_type);
+        exit(EXIT_FAILURE);
+    }
+
+    flash = qdev_new(flash_type);
+
+    if (dinfo) {
+        qdev_prop_set_drive(flash, "drive", blk_by_legacy_dinfo(dinfo));
+    }
+
+    qdev_realize_and_unref(flash, BUS(ssi->spi), &error_fatal);
+
+    flash_cs = qdev_get_gpio_in_named(flash, SSI_GPIO_CS, 0);
+    qdev_connect_gpio_out_named(DEVICE(ssi), "cs", cs, flash_cs);
+}
+
 static void k230_machine_done(Notifier *notifier, void *data)
 {
     K230MachineState *s = container_of(notifier, K230MachineState,
@@ -543,6 +589,12 @@ static void k230_machine_init(MachineState *machine)
                             TYPE_RISCV_K230_SOC);
     qdev_realize(DEVICE(&s->soc), NULL, &error_fatal);
 
+    if (s->spi_flash_model) {
+        k230_connect_spi_flash(&s->soc.dw_ssi[2], 0,
+                               s->spi_flash_model,
+                               drive_get(IF_MTD, 0, 0));
+    }
+
     /* Data Memory */
     memory_region_add_subregion(sys_mem, memmap[K230_DEV_DDRC].base,
                                 machine->ram);
@@ -555,6 +607,13 @@ static void k230_machine_instance_init(Object *obj)
 {
 }
 
+static void k230_machine_instance_finalize(Object *obj)
+{
+    K230MachineState *s = RISCV_K230_MACHINE(obj);
+
+    g_clear_pointer(&s->spi_flash_model, g_free);
+}
+
 static void k230_machine_class_init(ObjectClass *oc, const void *data)
 {
     MachineClass *mc = MACHINE_CLASS(oc);
@@ -564,6 +623,12 @@ static void k230_machine_class_init(ObjectClass *oc, const void *data)
     mc->default_cpus = 1;
     mc->default_ram_id = "riscv.K230.ram"; /* DDR */
     mc->default_ram_size = memmap[K230_DEV_DDRC].size;
+
+    object_class_property_add_str(oc, "spi-flash",
+                                  k230_machine_get_spi_flash,
+                                  k230_machine_set_spi_flash);
+    object_class_property_set_description(
+        oc, "spi-flash", "Attach an M25P80-compatible flash to spi0 CS0");
 }
 
 static const TypeInfo k230_machine_typeinfo = {
@@ -571,6 +636,7 @@ static const TypeInfo k230_machine_typeinfo = {
     .parent     = TYPE_MACHINE,
     .class_init = k230_machine_class_init,
     .instance_init = k230_machine_instance_init,
+    .instance_finalize = k230_machine_instance_finalize,
     .instance_size = sizeof(K230MachineState),
     .interfaces = riscv64_machine_interfaces,
 };
diff --git a/include/hw/riscv/k230.h b/include/hw/riscv/k230.h
index cfe30590cc..493d739aa4 100644
--- a/include/hw/riscv/k230.h
+++ b/include/hw/riscv/k230.h
@@ -52,6 +52,8 @@ typedef struct K230MachineState {
     /*< public >*/
     K230SoCState soc;
     Notifier machine_done;
+
+    char *spi_flash_model;
 } K230MachineState;
 
 enum {
diff --git a/tests/qtest/k230-dw-ssi-test.c b/tests/qtest/k230-dw-ssi-test.c
index a507814b66..658b1f4cbf 100644
--- a/tests/qtest/k230-dw-ssi-test.c
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -9,6 +9,7 @@
 #include "qemu/osdep.h"
 #include "libqtest.h"
 #include "qemu/bitops.h"
+#include "qemu/units.h"
 
 #define K230_SPI0_BASE          0x91584000ULL
 #define K230_SPI1_BASE          0x91582000ULL
@@ -68,9 +69,11 @@
 
 #define K230_SSI_SPI_CTRLR0_TRANS_TYPE(v) ((v) & 0x3U)
 #define K230_SSI_SPI_CTRLR0_ADDR_L(bits)  (((bits) / 4U) << 2)
+#define K230_SSI_SPI_CTRLR0_XIP_MD_EN     BIT(7)
 #define K230_SSI_SPI_CTRLR0_INST_L_8      (2U << 8)
 #define K230_SSI_SPI_CTRLR0_WAIT(v)       (((v) & 0x1fU) << 11)
 #define K230_SSI_SPI_CTRLR0_SPI_DDR_EN    BIT(16)
+#define K230_SSI_SPI_CTRLR0_XIP_MBL_8     (2U << 26)
 
 #define K230_SSI_SR_BUSY                BIT(0)
 #define K230_SSI_SR_TFNF                BIT(1)
@@ -88,6 +91,10 @@
 #define K230_SSI_IRQ_AXIE               8
 
 #define K230_SSI_FIFO_DEPTH             256
+#define K230_SSI_FLASH_IMAGE_SIZE       (32 * MiB)
+#define K230_SSI_FLASH_PATTERN_ADDR     0x100
+#define K230_SSI_FLASH_HIGH_ADDR        0x1000100
+#define K230_SSI_FLASH_PROGRAM_ADDR     0x22000
 
 typedef struct K230SsiInstance {
     uint64_t base;
@@ -96,6 +103,10 @@ typedef struct K230SsiInstance {
     uint32_t first_irq;
 } K230SsiInstance;
 
+typedef struct K230SsiFlashImage {
+    char *path;
+} K230SsiFlashImage;
+
 static const K230SsiInstance k230_ssi_instances[3] = {
     {
         .base = K230_SPI0_BASE,
@@ -117,6 +128,17 @@ static const K230SsiInstance k230_ssi_instances[3] = {
 
 #define FLASH_CMD_JEDEC         0x9f
 
+#define FLASH_CMD_WREN          0x06
+#define FLASH_CMD_RDSR          0x05
+#define FLASH_CMD_READ          0x03
+#define FLASH_CMD_READ4         0x13
+#define FLASH_CMD_QUAD_OUT      0x6b
+#define FLASH_CMD_QUAD_IO       0xeb
+#define FLASH_CMD_PP            0x02
+#define FLASH_SR_WIP            BIT(0)
+
+#define FLASH_CMD_QUAD_PP       0x32
+
 static QTestState *k230_ssi_start(void)
 {
     return qtest_init("-machine k230");
@@ -210,6 +232,188 @@ static bool k230_ssi_plic_pending(QTestState *qts, uint32_t irq)
     return qtest_readl(qts, addr) & BIT(irq % 32);
 }
 
+static void write_exact(int fd, const void *buf, size_t len, off_t offset)
+{
+    ssize_t ret = pwrite(fd, buf, len, offset);
+
+    g_assert_cmpint(ret, ==, len);
+}
+
+static void k230_ssi_flash_image_init(K230SsiFlashImage *image)
+{
+    static const uint8_t low_pattern[] = {
+        0xa5, 0x5a, 0x3c, 0xc3, 0x11, 0x22, 0x33, 0x44,
+    };
+    static const uint8_t high_pattern[] = { 0x71, 0x72, 0x73, 0x74 };
+    uint8_t erased[4096];
+    int fd;
+
+    memset(erased, 0xff, sizeof(erased));
+    image->path = NULL;
+    fd = g_file_open_tmp("qtest.k230.w25q256.XXXXXX", &image->path, NULL);
+    g_assert_cmpint(fd, >=, 0);
+    g_assert_cmpint(ftruncate(fd, K230_SSI_FLASH_IMAGE_SIZE), ==, 0);
+    write_exact(fd, low_pattern, sizeof(low_pattern),
+                K230_SSI_FLASH_PATTERN_ADDR);
+    write_exact(fd, high_pattern, sizeof(high_pattern),
+                K230_SSI_FLASH_HIGH_ADDR);
+    write_exact(fd, erased, sizeof(erased), K230_SSI_FLASH_PROGRAM_ADDR);
+    close(fd);
+}
+
+static void k230_ssi_flash_image_clear(K230SsiFlashImage *image)
+{
+    if (image->path) {
+        unlink(image->path);
+        g_clear_pointer(&image->path, g_free);
+    }
+}
+
+static QTestState *k230_ssi_start_with_flash(K230SsiFlashImage *image)
+{
+    k230_ssi_flash_image_init(image);
+    return qtest_initf("-machine k230,spi-flash=w25q256 "
+                       "-drive file=%s,format=raw,if=mtd",
+                       image->path);
+}
+
+static void flash_write_transaction(QTestState *qts,
+                                    const uint8_t *command,
+                                    size_t command_len)
+{
+    g_assert_nonnull(command);
+    g_assert_cmpuint(command_len, >, 0);
+    g_assert_cmpuint(command_len, <=, K230_SSI_FIFO_DEPTH);
+
+    k230_ssi_configure(qts, K230_SPI0_BASE, K230_SSI_TMOD_TO, 8, 0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, 0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SSIENR, 1);
+    for (size_t i = 0; i < command_len; i++) {
+        k230_ssi_write_frame(qts, K230_SPI0_BASE, command[i]);
+    }
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, BIT(0));
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_TXFLR,
+                       UINT32_MAX, 0);
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_SR,
+                       K230_SSI_SR_BUSY, 0);
+    k230_ssi_disable(qts, K230_SPI0_BASE);
+}
+
+static void flash_read_transaction(QTestState *qts,
+                                   const uint8_t *command,
+                                   size_t command_len,
+                                   uint8_t *data, size_t data_len)
+{
+    g_assert_nonnull(command);
+    g_assert_nonnull(data);
+    g_assert_cmpuint(command_len, >, 0);
+    g_assert_cmpuint(data_len, >, 0);
+    g_assert_cmpuint(data_len, <, K230_SSI_FIFO_DEPTH);
+
+    k230_ssi_configure(qts, K230_SPI0_BASE,
+                       K230_SSI_TMOD_EEPROM_READ, 8, data_len - 1);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, 0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SSIENR, 1);
+    for (size_t i = 0; i < command_len; i++) {
+        k230_ssi_write_frame(qts, K230_SPI0_BASE, command[i]);
+    }
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, BIT(0));
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_RXFLR,
+                       UINT32_MAX, data_len);
+    for (size_t i = 0; i < data_len; i++) {
+        data[i] = k230_ssi_read_frame(qts, K230_SPI0_BASE);
+    }
+    k230_ssi_disable(qts, K230_SPI0_BASE);
+}
+
+static void flash_read(QTestState *qts, uint8_t opcode, uint32_t address,
+                       unsigned int addr_bytes, uint8_t *data, size_t len)
+{
+    uint8_t command[5];
+
+    g_assert_cmpuint(addr_bytes, <=, 4);
+    command[0] = opcode;
+    for (unsigned int i = 0; i < addr_bytes; i++) {
+        command[1 + i] = address >> (8 * (addr_bytes - i - 1));
+    }
+    flash_read_transaction(qts, command, 1 + addr_bytes, data, len);
+}
+
+static uint8_t flash_read_status(QTestState *qts)
+{
+    uint8_t command = FLASH_CMD_RDSR;
+    uint8_t status;
+
+    flash_read_transaction(qts, &command, 1, &status, 1);
+    return status;
+}
+
+static void flash_wait_ready(QTestState *qts)
+{
+    for (int i = 0; i < 1000; i++) {
+        if (!(flash_read_status(qts) & FLASH_SR_WIP)) {
+            return;
+        }
+        qtest_clock_step(qts, 1000000);
+    }
+    g_assert_cmphex(flash_read_status(qts) & FLASH_SR_WIP, ==, 0);
+}
+
+static void flash_write_enable(QTestState *qts)
+{
+    uint8_t command = FLASH_CMD_WREN;
+
+    flash_write_transaction(qts, &command, 1);
+}
+
+static void configure_enhanced_transfer(QTestState *qts, uint32_t tmod,
+                                        uint32_t frf, uint32_t trans_type,
+                                        uint32_t wait_cycles,
+                                        bool mode_bits_enabled,
+                                        size_t data_frames)
+{
+    uint32_t ctrlr0;
+    uint32_t spi_ctrlr0;
+
+    k230_ssi_configure(qts, K230_SPI0_BASE, tmod, 8, data_frames - 1);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, 0);
+    ctrlr0 = k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_CTRLR0);
+    ctrlr0 &= ~K230_SSI_CTRLR0_SPI_FRF_MASK;
+    ctrlr0 |= frf << K230_SSI_CTRLR0_SPI_FRF_SHIFT;
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_CTRLR0, ctrlr0);
+
+    spi_ctrlr0 = K230_SSI_SPI_CTRLR0_TRANS_TYPE(trans_type) |
+                 K230_SSI_SPI_CTRLR0_ADDR_L(24) |
+                 K230_SSI_SPI_CTRLR0_INST_L_8 |
+                 K230_SSI_SPI_CTRLR0_WAIT(wait_cycles);
+    if (mode_bits_enabled) {
+        spi_ctrlr0 |= K230_SSI_SPI_CTRLR0_XIP_MD_EN |
+                      K230_SSI_SPI_CTRLR0_XIP_MBL_8;
+    }
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SPI_CTRLR0, spi_ctrlr0);
+}
+
+static void start_enhanced_transfer(QTestState *qts, uint8_t opcode,
+                                    uint32_t address)
+{
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SSIENR, 1);
+    k230_ssi_write_frame(qts, K230_SPI0_BASE, opcode);
+    k230_ssi_write_frame(qts, K230_SPI0_BASE, address);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, BIT(0));
+}
+
+static void read_enhanced_result(QTestState *qts, uint8_t *data, size_t len)
+{
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_RXFLR,
+                       UINT32_MAX, len);
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_SR,
+                       K230_SSI_SR_BUSY, 0);
+    for (size_t i = 0; i < len; i++) {
+        data[i] = k230_ssi_read_frame(qts, K230_SPI0_BASE);
+    }
+    k230_ssi_disable(qts, K230_SPI0_BASE);
+}
+
 static void test_register_contract(void)
 {
     QTestState *qts = k230_ssi_start();
@@ -390,6 +594,85 @@ static void test_qspi_config(void)
     qtest_quit(qts);
 }
 
+static void test_spi_nor(void)
+{
+    static const uint8_t expected[] = {
+        0xa5, 0x5a, 0x3c, 0xc3, 0x11, 0x22, 0x33, 0x44,
+    };
+    static const uint8_t payload[] = { 0xde, 0xad, 0xbe, 0xef };
+    K230SsiFlashImage image;
+    QTestState *qts = k230_ssi_start_with_flash(&image);
+    uint8_t command = FLASH_CMD_JEDEC;
+    uint8_t id[3];
+    uint8_t actual[ARRAY_SIZE(expected)];
+    uint8_t program[4 + ARRAY_SIZE(payload)];
+    uint32_t addr = K230_SSI_FLASH_PROGRAM_ADDR;
+
+    flash_read_transaction(qts, &command, 1, id, sizeof(id));
+    g_assert_cmphex(id[0], ==, 0xef);
+    g_assert_cmphex(id[1], ==, 0x40);
+    g_assert_cmphex(id[2], ==, 0x19);
+    flash_read(qts, FLASH_CMD_READ, K230_SSI_FLASH_PATTERN_ADDR,
+               3, actual, sizeof(actual));
+    g_assert_cmpmem(actual, sizeof(actual), expected, sizeof(expected));
+
+    flash_write_enable(qts);
+    program[0] = FLASH_CMD_PP;
+    program[1] = addr >> 16;
+    program[2] = addr >> 8;
+    program[3] = addr;
+    memcpy(program + 4, payload, sizeof(payload));
+    flash_write_transaction(qts, program, sizeof(program));
+    flash_wait_ready(qts);
+    flash_read(qts, FLASH_CMD_READ, addr, 3, actual, sizeof(payload));
+    g_assert_cmpmem(actual, sizeof(payload), payload, sizeof(payload));
+
+    qtest_quit(qts);
+    k230_ssi_flash_image_clear(&image);
+}
+
+static void test_qspi_sdr(void)
+{
+    static const uint8_t expected[] = { 0xa5, 0x5a, 0x3c, 0xc3 };
+    static const uint8_t payload[] = { 0x12, 0x34, 0x56, 0x78 };
+    K230SsiFlashImage image;
+    QTestState *qts = k230_ssi_start_with_flash(&image);
+    uint8_t actual[ARRAY_SIZE(expected)];
+
+    configure_enhanced_transfer(qts, K230_SSI_TMOD_RO,
+                                K230_SSI_FRF_QUAD, 0, 8, false,
+                                ARRAY_SIZE(actual));
+    start_enhanced_transfer(qts, FLASH_CMD_QUAD_OUT,
+                            K230_SSI_FLASH_PATTERN_ADDR);
+    read_enhanced_result(qts, actual, sizeof(actual));
+    g_assert_cmpmem(actual, sizeof(actual), expected, sizeof(expected));
+
+    flash_write_enable(qts);
+    configure_enhanced_transfer(qts, K230_SSI_TMOD_TO,
+                                K230_SSI_FRF_QUAD, 0, 0, false,
+                                ARRAY_SIZE(payload));
+    start_enhanced_transfer(qts, FLASH_CMD_QUAD_PP,
+                            K230_SSI_FLASH_PROGRAM_ADDR);
+    for (int i = 0; i < ARRAY_SIZE(payload); i++) {
+        k230_ssi_write_frame(qts, K230_SPI0_BASE, payload[i]);
+    }
+    k230_ssi_wait_mask(qts, K230_SPI0_BASE, K230_SSI_SR,
+                       K230_SSI_SR_BUSY, 0);
+    k230_ssi_disable(qts, K230_SPI0_BASE);
+    flash_wait_ready(qts);
+
+    configure_enhanced_transfer(qts, K230_SSI_TMOD_RO,
+                                K230_SSI_FRF_QUAD, 0, 8, false,
+                                ARRAY_SIZE(actual));
+    start_enhanced_transfer(qts, FLASH_CMD_QUAD_OUT,
+                            K230_SSI_FLASH_PROGRAM_ADDR);
+    read_enhanced_result(qts, actual, sizeof(actual));
+    g_assert_cmpmem(actual, sizeof(payload), payload, sizeof(payload));
+
+    qtest_quit(qts);
+    k230_ssi_flash_image_clear(&image);
+}
+
 int main(int argc, char **argv)
 {
     g_test_init(&argc, &argv, NULL);
@@ -400,5 +683,7 @@ int main(int argc, char **argv)
                    test_interrupt_controller);
     qtest_add_func("/k230-dw-ssi/plic-routing", test_plic_routing);
     qtest_add_func("/k230-dw-ssi/qspi-config", test_qspi_config);
+    qtest_add_func("/k230-dw-ssi/spi-nor", test_spi_nor);
+    qtest_add_func("/k230-dw-ssi/qspi-sdr", test_qspi_sdr);
     return g_test_run();
 }
-- 
2.43.0