Add a minimal K230 SDHCI model based on the generic SDHCI model. Reuse
the generic commands and data paths while providing the extended register
layout and documented reset values.

K230 SDHCI controller contains the standard SDHCI register block followed
by preset, extension pointer, PHY and vendor-specific registers. This model
implements the preset and extension pointer registers as read-only, retain
writes to most PHY and vendor-specific registers, and keep PHY_PWRGOOD
asserted so that the K230 SDK reset sequence can complete.

The SDHCI version is set to 3 due to the absence of ADMA3 support in the
generic model.

Signed-off-by: Xin Xie <[email protected]>
---
 MAINTAINERS                |   2 +
 hw/sd/Kconfig              |   4 +
 hw/sd/k230_sdhci.c         | 311 +++++++++++++++++++++++++++++++++++++
 hw/sd/meson.build          |   1 +
 include/hw/sd/k230_sdhci.h |  82 ++++++++++
 5 files changed, 400 insertions(+)
 create mode 100644 hw/sd/k230_sdhci.c
 create mode 100644 include/hw/sd/k230_sdhci.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 6171cc7494..06285352c4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1826,8 +1826,10 @@ S: Maintained
 F: docs/system/riscv/k230.rst
 F: hw/riscv/k230.c
 F: hw/watchdog/k230_wdt.c
+F: hw/sd/k230_sdhci.c
 F: include/hw/riscv/k230.h
 F: include/hw/watchdog/k230_wdt.h
+F: include/hw/sd/k230_sdhci.h
 F: tests/qtest/k230-wdt-test.c
 
 RX Machines
diff --git a/hw/sd/Kconfig b/hw/sd/Kconfig
index 633b9afec9..e9a5d63b4d 100644
--- a/hw/sd/Kconfig
+++ b/hw/sd/Kconfig
@@ -23,3 +23,7 @@ config SDHCI_PCI
 config CADENCE_SDHCI
     bool
     select SDHCI
+
+config K230_SDHCI
+    bool
+    select SDHCI
diff --git a/hw/sd/k230_sdhci.c b/hw/sd/k230_sdhci.c
new file mode 100644
index 0000000000..34c972a2f7
--- /dev/null
+++ b/hw/sd/k230_sdhci.c
@@ -0,0 +1,311 @@
+/*
+ * Kendryte K230 SDHCI controller
+ *
+ * Copyright (c) 2026 Xin Xie <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "hw/sd/k230_sdhci.h"
+#include "hw/sd/sdhci-internal.h"
+#include "migration/vmstate.h"
+
+/*
+ * The TRM describes an SDHCI v4.20 DWC MSHC, including the v4 block-count
+ * mode, ADMA3, and extension-area registers.  QEMU's generic controller
+ * supports up to v3 and does not implement those v4 transfer semantics.
+ */
+#define K230_SDHCI_SPEC_VERSION 3
+
+/*
+ * K230's Capabilities registers describe more functionality than the generic
+ * QEMU SDHCI data path currently implements.  Do not expose the following TRM
+ * capabilities to the guest:
+ *
+ * - ASYNC_INT requires an SDIO card-interrupt input and asynchronous interrupt
+ *   signalling while the SD clock is stopped.  SDBus has no such input.
+ * - TIMER_RETUNING and RETUNING_MODE require a re-tuning timer and generation
+ *   of a re-tuning request.  The generic controller only completes an initial
+ *   command-based tuning operation; it never schedules periodic re-tuning.
+ * - ADMA3 requires the SDHCI v4.20 ADMA3 descriptor and command semantics,
+ *   whereas the generic controller implements SDMA and ADMA2 only.
+ */
+#define K230_SDHCI_CAPABILITIES_UNSUPPORTED \
+    (R_SDHC_CAPAB_ASYNC_INT_MASK | \
+     R_SDHC_CAPAB_TIMER_RETUNING_MASK | \
+     R_SDHC_CAPAB_RETUNING_MODE_MASK | \
+     R_SDHC_CAPAB_ADMA3_MASK)
+#define K230_SDHCI_CAPAREG_IMPLEMENTED \
+    (K230_SDHCI_CAPAREG_RESET & ~K230_SDHCI_CAPABILITIES_UNSUPPORTED)
+
+#define K230_LO8(value)     ((value) & 0xff)
+#define K230_HI8(value)     (((value) >> 8) & 0xff)
+
+static const uint8_t k230_sdhci_preset_reset[K230_SDHCI_PRESET_SIZE] = {
+    [K230_SDHCI_PRESET_INIT - K230_SDHCI_PRESET_BASE] =
+        K230_LO8(K230_SDHCI_PRESET_INIT_RESET),
+    [K230_SDHCI_PRESET_INIT - K230_SDHCI_PRESET_BASE + 1] =
+        K230_HI8(K230_SDHCI_PRESET_INIT_RESET),
+    [K230_SDHCI_PRESET_DEFAULT_SPEED - K230_SDHCI_PRESET_BASE] =
+        K230_LO8(K230_SDHCI_PRESET_DEFAULT_SPEED_RESET),
+    [K230_SDHCI_PRESET_DEFAULT_SPEED - K230_SDHCI_PRESET_BASE + 1] =
+        K230_HI8(K230_SDHCI_PRESET_DEFAULT_SPEED_RESET),
+    [K230_SDHCI_PRESET_HIGH_SPEED - K230_SDHCI_PRESET_BASE] =
+        K230_LO8(K230_SDHCI_PRESET_HIGH_SPEED_RESET),
+    [K230_SDHCI_PRESET_HIGH_SPEED - K230_SDHCI_PRESET_BASE + 1] =
+        K230_HI8(K230_SDHCI_PRESET_HIGH_SPEED_RESET),
+    [K230_SDHCI_PRESET_SDR12 - K230_SDHCI_PRESET_BASE] =
+        K230_LO8(K230_SDHCI_PRESET_SDR12_RESET),
+    [K230_SDHCI_PRESET_SDR12 - K230_SDHCI_PRESET_BASE + 1] =
+        K230_HI8(K230_SDHCI_PRESET_SDR12_RESET),
+    [K230_SDHCI_PRESET_SDR25 - K230_SDHCI_PRESET_BASE] =
+        K230_LO8(K230_SDHCI_PRESET_SDR25_RESET),
+    [K230_SDHCI_PRESET_SDR25 - K230_SDHCI_PRESET_BASE + 1] =
+        K230_HI8(K230_SDHCI_PRESET_SDR25_RESET),
+    [K230_SDHCI_PRESET_SDR50 - K230_SDHCI_PRESET_BASE] =
+        K230_LO8(K230_SDHCI_PRESET_SDR50_RESET),
+    [K230_SDHCI_PRESET_SDR50 - K230_SDHCI_PRESET_BASE + 1] =
+        K230_HI8(K230_SDHCI_PRESET_SDR50_RESET),
+    [K230_SDHCI_PRESET_SDR104 - K230_SDHCI_PRESET_BASE] =
+        K230_LO8(K230_SDHCI_PRESET_SDR104_RESET),
+    [K230_SDHCI_PRESET_SDR104 - K230_SDHCI_PRESET_BASE + 1] =
+        K230_HI8(K230_SDHCI_PRESET_SDR104_RESET),
+    [K230_SDHCI_PRESET_DDR50 - K230_SDHCI_PRESET_BASE] =
+        K230_LO8(K230_SDHCI_PRESET_DDR50_RESET),
+    [K230_SDHCI_PRESET_DDR50 - K230_SDHCI_PRESET_BASE + 1] =
+        K230_HI8(K230_SDHCI_PRESET_DDR50_RESET),
+};
+
+static uint64_t k230_sdhci_preset_read(void *opaque, hwaddr addr,
+                                      unsigned int size)
+{
+    return ldn_le_p(&k230_sdhci_preset_reset[addr], size);
+}
+
+static void k230_sdhci_preset_write(void *opaque, hwaddr addr, uint64_t value,
+                                   unsigned int size)
+{
+    qemu_log_mask(LOG_GUEST_ERROR,
+                  "K230 SDHCI: write to read-only Preset Value register "
+                  "0x%03" HWADDR_PRIx "\n",
+                  K230_SDHCI_PRESET_BASE + addr);
+}
+
+static const MemoryRegionOps k230_sdhci_preset_ops = {
+    .read = k230_sdhci_preset_read,
+    .write = k230_sdhci_preset_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+};
+
+static const uint8_t k230_sdhci_pointer_reset[K230_SDHCI_POINTER_SIZE] = {
+    [K230_SDHCI_P_UHS2_SETTINGS - K230_SDHCI_POINTER_BASE] =
+        K230_LO8(K230_SDHCI_P_UHS2_SETTINGS_RESET),
+    [K230_SDHCI_P_UHS2_SETTINGS - K230_SDHCI_POINTER_BASE + 1] =
+        K230_HI8(K230_SDHCI_P_UHS2_SETTINGS_RESET),
+    [K230_SDHCI_P_UHS2_HOST_CAPAB - K230_SDHCI_POINTER_BASE] =
+        K230_LO8(K230_SDHCI_P_UHS2_HOST_CAPAB_RESET),
+    [K230_SDHCI_P_UHS2_HOST_CAPAB - K230_SDHCI_POINTER_BASE + 1] =
+        K230_HI8(K230_SDHCI_P_UHS2_HOST_CAPAB_RESET),
+    [K230_SDHCI_P_UHS2_TEST - K230_SDHCI_POINTER_BASE] =
+        K230_LO8(K230_SDHCI_P_UHS2_TEST_RESET),
+    [K230_SDHCI_P_UHS2_TEST - K230_SDHCI_POINTER_BASE + 1] =
+        K230_HI8(K230_SDHCI_P_UHS2_TEST_RESET),
+    [K230_SDHCI_P_EMBEDDED_CNTRL - K230_SDHCI_POINTER_BASE] =
+        K230_LO8(K230_SDHCI_P_EMBEDDED_CNTRL_RESET),
+    [K230_SDHCI_P_EMBEDDED_CNTRL - K230_SDHCI_POINTER_BASE + 1] =
+        K230_HI8(K230_SDHCI_P_EMBEDDED_CNTRL_RESET),
+    [K230_SDHCI_P_VENDOR_AREA1 - K230_SDHCI_POINTER_BASE] =
+        K230_LO8(K230_SDHCI_P_VENDOR_AREA1_RESET),
+    [K230_SDHCI_P_VENDOR_AREA1 - K230_SDHCI_POINTER_BASE + 1] =
+        K230_HI8(K230_SDHCI_P_VENDOR_AREA1_RESET),
+    [K230_SDHCI_P_VENDOR_AREA2 - K230_SDHCI_POINTER_BASE] =
+        K230_LO8(K230_SDHCI_P_VENDOR_AREA2_RESET),
+    [K230_SDHCI_P_VENDOR_AREA2 - K230_SDHCI_POINTER_BASE + 1] =
+        K230_HI8(K230_SDHCI_P_VENDOR_AREA2_RESET),
+};
+
+static uint64_t k230_sdhci_pointer_read(void *opaque, hwaddr addr,
+                                       unsigned int size)
+{
+    return ldn_le_p(&k230_sdhci_pointer_reset[addr], size);
+}
+
+static void k230_sdhci_pointer_write(void *opaque, hwaddr addr, uint64_t value,
+                                    unsigned int size)
+{
+    qemu_log_mask(LOG_GUEST_ERROR,
+                  "K230 SDHCI: write to read-only pointer register "
+                  "0x%03" HWADDR_PRIx "\n",
+                  K230_SDHCI_POINTER_BASE + addr);
+}
+
+static const MemoryRegionOps k230_sdhci_pointer_ops = {
+    .read = k230_sdhci_pointer_read,
+    .write = k230_sdhci_pointer_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+};
+
+/*
+ * K230 uses the standard SDHCI register set at offset zero, followed by
+ * UHS-II, DWC MSHC PHY, Embedded Control, and vendor-specific blocks. Most
+ * side effects of analogue PHY behaviors not simulated.
+ *
+ * PHY_PWRGOOD is the sole synthesized status bit.  Real hardware asserts it
+ * after PHY power and reset sequencing; the SDK driver polls it during every
+ * full controller reset.  A virtual PHY has no settling interval or failure
+ * condition, so the bit is permanently asserted and cannot be cleared by a
+ * guest write.
+ */
+static uint64_t k230_sdhci_fallback_read(void *opaque, hwaddr addr,
+                                        unsigned int size)
+{
+    K230SDHCIState *s = opaque;
+    uint64_t value = ldn_le_p(&s->fallback_regs[addr], size);
+
+    if (ranges_overlap(addr, size, K230_SDHCI_PHY_CNFG,
+                       sizeof(uint32_t)) && addr <= K230_SDHCI_PHY_CNFG) {
+        unsigned int shift = (K230_SDHCI_PHY_CNFG - addr) * 8;
+
+        value |= (uint64_t)K230_SDHCI_PHY_CNFG_PWRGOOD << shift;
+    }
+
+    return value;
+}
+
+static void k230_sdhci_fallback_write(void *opaque, hwaddr addr,
+                                     uint64_t value, unsigned int size)
+{
+    K230SDHCIState *s = opaque;
+
+    stn_le_p(&s->fallback_regs[addr], size, value);
+
+    /* PWRGOOD reflects PHY state and is not software writable. */
+    s->fallback_regs[K230_SDHCI_PHY_CNFG] |=
+        K230_SDHCI_PHY_CNFG_PWRGOOD;
+}
+
+static const MemoryRegionOps k230_sdhci_fallback_ops = {
+    .read = k230_sdhci_fallback_read,
+    .write = k230_sdhci_fallback_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+};
+
+static void k230_sdhci_reset(DeviceState *dev)
+{
+    K230SDHCIState *s = K230_SDHCI(dev);
+
+    memset(s->fallback_regs, 0, sizeof(s->fallback_regs));
+    s->fallback_regs[K230_SDHCI_PHY_CNFG] =
+        K230_SDHCI_PHY_CNFG_PWRGOOD;
+    device_cold_reset(DEVICE(&s->sdhci));
+}
+
+static void k230_sdhci_realize(DeviceState *dev, Error **errp)
+{
+    K230SDHCIState *s = K230_SDHCI(dev);
+    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+    SysBusDevice *sdhci_sbd = SYS_BUS_DEVICE(&s->sdhci);
+
+    memory_region_init(&s->container, OBJECT(s), "k230.sdhci-container",
+                       K230_SDHCI_REG_SIZE);
+    sysbus_init_mmio(sbd, &s->container);
+
+    memory_region_init_io(&s->iomem_fallback, OBJECT(s),
+                          &k230_sdhci_fallback_ops, s,
+                          "k230.sdhci-fallback",
+                          K230_SDHCI_REG_SIZE);
+    memory_region_add_subregion(&s->container, 0, &s->iomem_fallback);
+
+    if (!sysbus_realize(sdhci_sbd, errp)) {
+        return;
+    }
+    memory_region_add_subregion_overlap(
+        &s->container, 0, sysbus_mmio_get_region(sdhci_sbd, 0), 1);
+
+    memory_region_init_io(&s->iomem_preset, OBJECT(s),
+                          &k230_sdhci_preset_ops, s,
+                          "k230.sdhci-preset", K230_SDHCI_PRESET_SIZE);
+    memory_region_add_subregion_overlap(&s->container,
+                                        K230_SDHCI_PRESET_BASE,
+                                        &s->iomem_preset, 2);
+
+    memory_region_init_io(&s->iomem_pointer, OBJECT(s),
+                          &k230_sdhci_pointer_ops, s,
+                          "k230.sdhci-pointers", K230_SDHCI_POINTER_SIZE);
+    memory_region_add_subregion_overlap(&s->container,
+                                        K230_SDHCI_POINTER_BASE,
+                                        &s->iomem_pointer, 2);
+
+    sysbus_pass_irq(sbd, sdhci_sbd);
+    s->bus = qdev_get_child_bus(DEVICE(sdhci_sbd), "sd-bus");
+}
+
+static const VMStateDescription vmstate_k230_sdhci = {
+    .name = TYPE_K230_SDHCI,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT8_ARRAY(fallback_regs, K230SDHCIState,
+                            K230_SDHCI_REG_SIZE),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void k230_sdhci_instance_init(Object *obj)
+{
+    K230SDHCIState *s = K230_SDHCI(obj);
+
+    object_initialize_child(obj, "generic-sdhci", &s->sdhci,
+                            TYPE_SYSBUS_SDHCI);
+
+    object_property_set_uint(OBJECT(&s->sdhci), "sd-spec-version",
+                             K230_SDHCI_SPEC_VERSION,
+                             &error_abort);
+    object_property_set_uint(OBJECT(&s->sdhci), "capareg",
+                             K230_SDHCI_CAPAREG_IMPLEMENTED,
+                             &error_abort);
+    object_property_set_uint(OBJECT(&s->sdhci), "uhs", UHS_I, &error_abort);
+}
+
+static void k230_sdhci_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->desc = "Kendryte K230 SDHCI Controller";
+    dc->realize = k230_sdhci_realize;
+    device_class_set_legacy_reset(dc, k230_sdhci_reset);
+    dc->vmsd = &vmstate_k230_sdhci;
+    set_bit(DEVICE_CATEGORY_STORAGE, dc->categories);
+}
+
+static const TypeInfo k230_sdhci_type_info = {
+    .name = TYPE_K230_SDHCI,
+    .parent = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(K230SDHCIState),
+    .instance_init = k230_sdhci_instance_init,
+    .class_init = k230_sdhci_class_init,
+};
+
+static void k230_sdhci_register_types(void)
+{
+    type_register_static(&k230_sdhci_type_info);
+}
+
+type_init(k230_sdhci_register_types)
diff --git a/hw/sd/meson.build b/hw/sd/meson.build
index b43d45bc56..a109635233 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_SDHCI', if_true: files('k230_sdhci.c'))
diff --git a/include/hw/sd/k230_sdhci.h b/include/hw/sd/k230_sdhci.h
new file mode 100644
index 0000000000..a8389f1de4
--- /dev/null
+++ b/include/hw/sd/k230_sdhci.h
@@ -0,0 +1,82 @@
+/*
+ * Kendryte K230 SDHCI controller
+ *
+ * Copyright (c) 2026 Xin Xie <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_SD_K230_SDHCI_H
+#define HW_SD_K230_SDHCI_H
+
+#include "qemu/bitops.h"
+#include "hw/sd/sdhci.h"
+#include "qom/object.h"
+
+#define TYPE_K230_SDHCI "k230.sdhci"
+OBJECT_DECLARE_SIMPLE_TYPE(K230SDHCIState, K230_SDHCI)
+
+#define K230_SDHCI_REG_SIZE             0x1000
+
+/* K230 TRM reset values for Capabilities Registers 1 and 2. */
+#define K230_SDHCI_CAPABILITIES1_RESET  0x256ec881ULL
+#define K230_SDHCI_CAPABILITIES2_RESET  0x0800a177ULL
+#define K230_SDHCI_CAPAREG_RESET \
+    ((K230_SDHCI_CAPABILITIES2_RESET << 32) \
+    | K230_SDHCI_CAPABILITIES1_RESET)
+
+/* SDHCI Preset Value registers. */
+#define K230_SDHCI_PRESET_BASE           0x060
+#define K230_SDHCI_PRESET_SIZE           0x010
+#define K230_SDHCI_PRESET_INIT           0x060
+#define K230_SDHCI_PRESET_DEFAULT_SPEED  0x062
+#define K230_SDHCI_PRESET_HIGH_SPEED     0x064
+#define K230_SDHCI_PRESET_SDR12          0x066
+#define K230_SDHCI_PRESET_SDR25          0x068
+#define K230_SDHCI_PRESET_SDR50          0x06a
+#define K230_SDHCI_PRESET_SDR104         0x06c
+#define K230_SDHCI_PRESET_DDR50          0x06e
+#define K230_SDHCI_PRESET_INIT_RESET          0x00fa
+#define K230_SDHCI_PRESET_DEFAULT_SPEED_RESET 0x0004
+#define K230_SDHCI_PRESET_HIGH_SPEED_RESET    0x0002
+#define K230_SDHCI_PRESET_SDR12_RESET         0x0004
+#define K230_SDHCI_PRESET_SDR25_RESET         0x0002
+#define K230_SDHCI_PRESET_SDR50_RESET         0x0001
+#define K230_SDHCI_PRESET_SDR104_RESET        0x0000
+#define K230_SDHCI_PRESET_DDR50_RESET         0x0002
+
+/* SDHCI v4 extension-area pointer registers. */
+#define K230_SDHCI_POINTER_BASE         0x0e0
+#define K230_SDHCI_POINTER_SIZE         0x00c
+#define K230_SDHCI_P_UHS2_SETTINGS      0x0e0
+#define K230_SDHCI_P_UHS2_HOST_CAPAB    0x0e2
+#define K230_SDHCI_P_UHS2_TEST          0x0e4
+#define K230_SDHCI_P_EMBEDDED_CNTRL     0x0e6
+#define K230_SDHCI_P_VENDOR_AREA1       0x0e8
+#define K230_SDHCI_P_VENDOR_AREA2       0x0ea
+#define K230_SDHCI_P_UHS2_SETTINGS_RESET    0x0000
+#define K230_SDHCI_P_UHS2_HOST_CAPAB_RESET  0x0f58
+#define K230_SDHCI_P_UHS2_TEST_RESET        0x0f68
+#define K230_SDHCI_P_EMBEDDED_CNTRL_RESET   0x0f6c
+#define K230_SDHCI_P_VENDOR_AREA1_RESET     0x0500
+#define K230_SDHCI_P_VENDOR_AREA2_RESET     0x0384
+
+/* DWC MSHC PHY register block. */
+#define K230_SDHCI_PHY_CNFG             0x300
+#define K230_SDHCI_PHY_CNFG_RSTN        BIT(0)
+#define K230_SDHCI_PHY_CNFG_PWRGOOD     BIT(1)
+
+struct K230SDHCIState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion container;
+    MemoryRegion iomem_fallback;
+    MemoryRegion iomem_preset;
+    MemoryRegion iomem_pointer;
+    BusState *bus;
+    uint8_t fallback_regs[K230_SDHCI_REG_SIZE];
+
+    SDHCIState sdhci;
+};
+
+#endif
-- 
2.43.0


Reply via email to