[PATCH 4/5] hw/sd: add K230 SD/eMMC (DesignWare dw_mmc).

Leo Cheng <[email protected]>
Newsgroups gmane.comp.emulators.qemu
Message-ID <[email protected]>
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.