Model the register interface of the DesignWare dw_mmc host used by the K230: 
self-clearing CTRL reset bits, a self-clearing CMD.START that raises command 
completion, W1C RINTSTS and masked-interrupt delivery. The FIFO/DMA data path 
is not modelled; this is enough for a driver to probe. Register map from the 
DesignWare dw_mmc driver. Covered by tests/qtest/k230-dwmmc-test.

Signed-off-by: Leo Cheng <[email protected]>
---
 hw/sd/Kconfig                 |   3 +
 hw/sd/k230_dwmmc.c            | 139 ++++++++++++++++++++++++++++++++++
 hw/sd/meson.build             |   1 +
 hw/sd/trace-events            |   4 +
 include/hw/sd/k230_dwmmc.h    |  61 +++++++++++++++
 tests/qtest/k230-dwmmc-test.c |  79 +++++++++++++++++++
 6 files changed, 287 insertions(+)
 create mode 100644 hw/sd/k230_dwmmc.c
 create mode 100644 include/hw/sd/k230_dwmmc.h
 create mode 100644 tests/qtest/k230-dwmmc-test.c

diff --git a/hw/sd/Kconfig b/hw/sd/Kconfig
index 633b9af..15f1fd9 100644
--- a/hw/sd/Kconfig
+++ b/hw/sd/Kconfig
@@ -23,3 +23,6 @@ config SDHCI_PCI
 config CADENCE_SDHCI
     bool
     select SDHCI
+
+config K230_DWMMC
+    bool
diff --git a/hw/sd/k230_dwmmc.c b/hw/sd/k230_dwmmc.c
new file mode 100644
index 0000000..47a68a0
--- /dev/null
+++ b/hw/sd/k230_dwmmc.c
@@ -0,0 +1,139 @@
+/*
+ * K230 SD/eMMC host (Synopsys DesignWare dw_mmc)
+ *
+ * Copyright (c) 2026 Leo Cheng <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Register map from the DesignWare dw_mmc driver (Canaan k230_sdk). Models the
+ * register interface and command completion; the FIFO/DMA data path is not
+ * modelled.
+ */
+#include "qemu/osdep.h"
+#include "qemu/module.h"
+#include "hw/core/sysbus.h"
+#include "hw/core/irq.h"
+#include "migration/vmstate.h"
+#include "hw/sd/k230_dwmmc.h"
+#include "trace.h"
+
+static void k230_dwmmc_update_irq(K230DwmmcState *s)
+{
+    uint32_t masked = s->regs[SDMMC_RINTSTS / 4] & s->regs[SDMMC_INTMASK / 4];
+
+    s->regs[SDMMC_MINTSTS / 4] = masked;
+    qemu_set_irq(s->irq, masked != 0);
+}
+
+static uint64_t k230_dwmmc_read(void *opaque, hwaddr addr, unsigned int size)
+{
+    K230DwmmcState *s = K230_DWMMC(opaque);
+    uint32_t value = addr / 4 < K230_DWMMC_NREGS ? s->regs[addr / 4] : 0;
+
+    trace_k230_dwmmc_read(addr, value);
+    return value;
+}
+
+static void k230_dwmmc_write(void *opaque, hwaddr addr,
+                             uint64_t value, unsigned int size)
+{
+    K230DwmmcState *s = K230_DWMMC(opaque);
+
+    if (addr / 4 >= K230_DWMMC_NREGS) {
+        return;
+    }
+    trace_k230_dwmmc_write(addr, value);
+
+    switch (addr) {
+    case SDMMC_CTRL:
+        /* the three reset bits are self-clearing */
+        s->regs[SDMMC_CTRL / 4] = value & ~SDMMC_CTRL_RESET_ALL;
+        break;
+    case SDMMC_CMD:
+        /* START self-clears; completing a command raises command-done */
+        s->regs[SDMMC_CMD / 4] = value & ~SDMMC_CMD_START;
+        if (value & SDMMC_CMD_START) {
+            s->regs[SDMMC_RINTSTS / 4] |= SDMMC_INT_CMD_DONE;
+            k230_dwmmc_update_irq(s);
+        }
+        break;
+    case SDMMC_RINTSTS:
+        s->regs[SDMMC_RINTSTS / 4] &= ~(uint32_t)value;   /* W1C */
+        k230_dwmmc_update_irq(s);
+        break;
+    case SDMMC_INTMASK:
+        s->regs[SDMMC_INTMASK / 4] = value;
+        k230_dwmmc_update_irq(s);
+        break;
+    case SDMMC_STATUS:
+    case SDMMC_MINTSTS:
+        break;                                            /* read-only */
+    default:
+        s->regs[addr / 4] = value;
+        break;
+    }
+}
+
+static const MemoryRegionOps k230_dwmmc_ops = {
+    .read = k230_dwmmc_read,
+    .write = k230_dwmmc_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .impl = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+};
+
+static void k230_dwmmc_reset(DeviceState *dev)
+{
+    K230DwmmcState *s = K230_DWMMC(dev);
+
+    memset(s->regs, 0, sizeof(s->regs));
+    s->regs[SDMMC_STATUS / 4] = SDMMC_STATUS_FIFO_EMPTY;
+    qemu_set_irq(s->irq, 0);
+}
+
+static const VMStateDescription vmstate_k230_dwmmc = {
+    .name = "k230.dwmmc",
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32_ARRAY(regs, K230DwmmcState, K230_DWMMC_NREGS),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void k230_dwmmc_realize(DeviceState *dev, Error **errp)
+{
+    K230DwmmcState *s = K230_DWMMC(dev);
+    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+
+    memory_region_init_io(&s->mmio, OBJECT(dev), &k230_dwmmc_ops, s,
+                          TYPE_K230_DWMMC, K230_DWMMC_MMIO_SIZE);
+    sysbus_init_mmio(sbd, &s->mmio);
+    sysbus_init_irq(sbd, &s->irq);
+    s->regs[SDMMC_STATUS / 4] = SDMMC_STATUS_FIFO_EMPTY;
+}
+
+static void k230_dwmmc_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->realize = k230_dwmmc_realize;
+    device_class_set_legacy_reset(dc, k230_dwmmc_reset);
+    dc->vmsd = &vmstate_k230_dwmmc;
+    dc->desc = "K230 SD/eMMC (dw_mmc)";
+}
+
+static const TypeInfo k230_dwmmc_info = {
+    .name          = TYPE_K230_DWMMC,
+    .parent        = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(K230DwmmcState),
+    .class_init    = k230_dwmmc_class_init,
+};
+
+static void k230_dwmmc_register_type(void)
+{
+    type_register_static(&k230_dwmmc_info);
+}
+
+type_init(k230_dwmmc_register_type)
diff --git a/hw/sd/meson.build b/hw/sd/meson.build
index b43d45b..d642de3 100644
--- a/hw/sd/meson.build
+++ b/hw/sd/meson.build
@@ -10,3 +10,4 @@ system_ss.add(when: 'CONFIG_ASPEED_SOC', if_true: 
files('aspeed_sdhci.c'))
 system_ss.add(when: 'CONFIG_ALLWINNER_H3', if_true: 
files('allwinner-sdhost.c'))
 system_ss.add(when: 'CONFIG_NPCM7XX', if_true: files('npcm7xx_sdhci.c'))
 system_ss.add(when: 'CONFIG_CADENCE_SDHCI', if_true: files('cadence_sdhci.c'))
+system_ss.add(when: 'CONFIG_K230_DWMMC', if_true: files('k230_dwmmc.c'))
diff --git a/hw/sd/trace-events b/hw/sd/trace-events
index c6811f3..8537b25 100644
--- a/hw/sd/trace-events
+++ b/hw/sd/trace-events
@@ -75,3 +75,7 @@ pl181_data_engine_idle(void) "data engine idle"
 # aspeed_sdhci.c
 aspeed_sdhci_read(uint64_t addr, uint32_t size, uint64_t data) "@0x%" PRIx64 " 
size %u: 0x%" PRIx64
 aspeed_sdhci_write(uint64_t addr, uint32_t size, uint64_t data) "@0x%" PRIx64 
" size %u: 0x%" PRIx64
+
+# k230_dwmmc.c
+k230_dwmmc_read(uint64_t addr, uint32_t val) "addr 0x%" PRIx64 " val 0x%08x"
+k230_dwmmc_write(uint64_t addr, uint64_t val) "addr 0x%" PRIx64 " val 0x%" 
PRIx64
diff --git a/include/hw/sd/k230_dwmmc.h b/include/hw/sd/k230_dwmmc.h
new file mode 100644
index 0000000..6c5c255
--- /dev/null
+++ b/include/hw/sd/k230_dwmmc.h
@@ -0,0 +1,61 @@
+/*
+ * K230 SD/eMMC host (Synopsys DesignWare dw_mmc)
+ *
+ * Copyright (c) 2026 Leo Cheng <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Register map from the DesignWare dw_mmc driver (Canaan k230_sdk
+ * drivers/mmc/host/dw_mmc.h). This models the register interface and command
+ * completion enough for a driver to probe; the FIFO/DMA data path is not
+ * modelled (documented limitation).
+ */
+#ifndef HW_SD_K230_DWMMC_H
+#define HW_SD_K230_DWMMC_H
+
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+
+#define TYPE_K230_DWMMC "riscv.k230.dwmmc"
+OBJECT_DECLARE_SIMPLE_TYPE(K230DwmmcState, K230_DWMMC)
+
+#define K230_DWMMC_MMIO_SIZE 0x1000
+#define K230_DWMMC_NREGS     (0x100 / 4)
+
+#define SDMMC_CTRL    0x000
+#define SDMMC_PWREN   0x004
+#define SDMMC_CLKDIV  0x008
+#define SDMMC_CLKENA  0x010
+#define SDMMC_TMOUT   0x014
+#define SDMMC_CTYPE   0x018
+#define SDMMC_BLKSIZ  0x01c
+#define SDMMC_BYTCNT  0x020
+#define SDMMC_INTMASK 0x024
+#define SDMMC_CMDARG  0x028
+#define SDMMC_CMD     0x02c
+#define SDMMC_RESP0   0x030
+#define SDMMC_RESP3   0x03c
+#define SDMMC_MINTSTS 0x040
+#define SDMMC_RINTSTS 0x044
+#define SDMMC_STATUS  0x048
+
+#define SDMMC_CTRL_RESET      (1u << 0)
+#define SDMMC_CTRL_FIFO_RESET (1u << 1)
+#define SDMMC_CTRL_DMA_RESET  (1u << 2)
+#define SDMMC_CTRL_RESET_ALL  (SDMMC_CTRL_RESET | SDMMC_CTRL_FIFO_RESET | \
+                               SDMMC_CTRL_DMA_RESET)
+#define SDMMC_CMD_START       (1u << 31)
+#define SDMMC_INT_CMD_DONE    (1u << 2)
+
+/* STATUS: FIFO empty so a polling driver does not stall */
+#define SDMMC_STATUS_FIFO_EMPTY (1u << 2)
+
+struct K230DwmmcState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion mmio;
+    qemu_irq irq;
+    uint32_t regs[K230_DWMMC_NREGS];
+};
+
+#endif
diff --git a/tests/qtest/k230-dwmmc-test.c b/tests/qtest/k230-dwmmc-test.c
new file mode 100644
index 0000000..1adeef0
--- /dev/null
+++ b/tests/qtest/k230-dwmmc-test.c
@@ -0,0 +1,79 @@
+/*
+ * QTest testcase for K230 SD/eMMC (dw_mmc)
+ *
+ * Copyright (c) 2026 Leo Cheng <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+#include "qemu/osdep.h"
+#include "libqtest.h"
+#include "hw/sd/k230_dwmmc.h"
+
+#define SD0_BASE     0x91580000
+#define PLIC_BASE    0xF00000000ULL
+#define PLIC_PENDING (PLIC_BASE + 0x1000)
+#define SD0_IRQ      142
+
+static bool plic_pending(QTestState *qts, int irq)
+{
+    uint32_t word = qtest_readl(qts, PLIC_PENDING + (irq / 32) * 4);
+    return (word >> (irq % 32)) & 1;
+}
+
+static void test_reset_values(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    g_assert_cmphex(qtest_readl(qts, SD0_BASE + SDMMC_CTRL), ==, 0);
+    g_assert_cmphex(qtest_readl(qts, SD0_BASE + SDMMC_STATUS), ==,
+                    SDMMC_STATUS_FIFO_EMPTY);
+
+    qtest_quit(qts);
+}
+
+/* The three CTRL reset bits are self-clearing */
+static void test_ctrl_reset_selfclear(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    qtest_writel(qts, SD0_BASE + SDMMC_CTRL, SDMMC_CTRL_RESET_ALL | 0x10);
+    g_assert_cmphex(qtest_readl(qts, SD0_BASE + SDMMC_CTRL) &
+                    SDMMC_CTRL_RESET_ALL, ==, 0);
+    g_assert_cmphex(qtest_readl(qts, SD0_BASE + SDMMC_CTRL) & 0x10, ==, 0x10);
+
+    qtest_quit(qts);
+}
+
+/* Starting a command auto-clears START, raises command-done, W1C clears it */
+static void test_cmd_done(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    qtest_writel(qts, SD0_BASE + SDMMC_INTMASK, SDMMC_INT_CMD_DONE);
+    qtest_writel(qts, SD0_BASE + SDMMC_CMD, SDMMC_CMD_START | 0x1);
+
+    g_assert_cmphex(qtest_readl(qts, SD0_BASE + SDMMC_CMD) & SDMMC_CMD_START,
+                    ==, 0);
+    g_assert_cmphex(qtest_readl(qts, SD0_BASE + SDMMC_RINTSTS) &
+                    SDMMC_INT_CMD_DONE, ==, SDMMC_INT_CMD_DONE);
+    g_assert_true(plic_pending(qts, SD0_IRQ));
+
+    /* RINTSTS is write-1-to-clear */
+    qtest_writel(qts, SD0_BASE + SDMMC_RINTSTS, SDMMC_INT_CMD_DONE);
+    g_assert_cmphex(qtest_readl(qts, SD0_BASE + SDMMC_RINTSTS) &
+                    SDMMC_INT_CMD_DONE, ==, 0);
+
+    qtest_quit(qts);
+}
+
+int main(int argc, char **argv)
+{
+    g_test_init(&argc, &argv, NULL);
+
+    qtest_add_func("/k230-dwmmc/reset_values", test_reset_values);
+    qtest_add_func("/k230-dwmmc/ctrl_reset_selfclear",
+                   test_ctrl_reset_selfclear);
+    qtest_add_func("/k230-dwmmc/cmd_done", test_cmd_done);
+
+    return g_test_run();
+}
-- 
2.43.0


Reply via email to