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
