From 25c49f7f5cccd1bbc6a478db703a553b1b8ab875 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Tue, 14 Jul 2026 07:40:30 +0800 Subject: [PATCH 01/13] target/riscv: add SpacemiT X100 CPU type SpacemiT K3 uses eight X100 application harts implementing RVA23S64. Add a spacemit-x100 vendor CPU with RVH, a 256-bit VLEN, Sv39, and AIA support so board models can select the hardware CPU directly. Signed-off-by: Chao Liu --- target/riscv/cpu-qom.h | 1 + target/riscv/cpu.c | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/target/riscv/cpu-qom.h b/target/riscv/cpu-qom.h index 61234842e3cb3..7bdca8e35dabf 100644 --- a/target/riscv/cpu-qom.h +++ b/target/riscv/cpu-qom.h @@ -45,6 +45,7 @@ #define TYPE_RISCV_CPU_RVSERVER_REF RISCV_CPU_TYPE_NAME("riscv-server-ref") #define TYPE_RISCV_CPU_IBEX RISCV_CPU_TYPE_NAME("lowrisc-ibex") #define TYPE_RISCV_CPU_SHAKTI_C RISCV_CPU_TYPE_NAME("shakti-c") +#define TYPE_RISCV_CPU_SPACEMIT_X100 RISCV_CPU_TYPE_NAME("spacemit-x100") #define TYPE_RISCV_CPU_SIFIVE_E RISCV_CPU_TYPE_NAME("sifive-e") #define TYPE_RISCV_CPU_SIFIVE_E31 RISCV_CPU_TYPE_NAME("sifive-e31") #define TYPE_RISCV_CPU_SIFIVE_E34 RISCV_CPU_TYPE_NAME("sifive-e34") diff --git a/target/riscv/cpu.c b/target/riscv/cpu.c index 1c871723f8540..68c9692314189 100644 --- a/target/riscv/cpu.c +++ b/target/riscv/cpu.c @@ -3483,6 +3483,18 @@ static const TypeInfo riscv_cpu_type_infos[] = { DEFINE_PROFILE_CPU(TYPE_RISCV_CPU_RVA22S64, TYPE_RISCV_CPU_RV64I, RVA22S64), DEFINE_PROFILE_CPU(TYPE_RISCV_CPU_RVA23U64, TYPE_RISCV_CPU_RV64I, RVA23U64), DEFINE_PROFILE_CPU(TYPE_RISCV_CPU_RVA23S64, TYPE_RISCV_CPU_RV64I, RVA23S64), + + DEFINE_RISCV_CPU(TYPE_RISCV_CPU_SPACEMIT_X100, + TYPE_RISCV_VENDOR_CPU, + .profile = &RVA23S64, + .misa_mxl_max = MXL_RV64, + .misa_ext = RVH, + .cfg.mmu = true, + .cfg.vlenb = 256 >> 3, + .cfg.ext_smaia = true, + .cfg.ext_ssaia = true, + .cfg.max_satp_mode = VM_1_10_SV39, + ), #endif /* TARGET_RISCV64 */ }; From 9ef510a6b14096214caf6ccd10012ce89b2639ad Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Tue, 14 Jul 2026 06:42:09 +0800 Subject: [PATCH 02/13] hw/riscv: add SpacemiT K3 Pico-ITX machine Model the Linux-visible X100 subset with two four-hart RVA23S64 clusters, fixed 2 GiB RAM, a 24 MHz ACLINT, M/S APLIC and IMSIC controllers, and UART0. Use the dedicated spacemit-x100 CPU type and keep H disabled for this Linux-first subset because the VS interrupt path is not modeled. Support direct Linux boot through generic OpenSBI and an external device tree. Keep the A100 harts and vendor firmware path outside the current machine contract. Signed-off-by: Chao Liu --- MAINTAINERS | 7 + configs/devices/riscv64-softmmu/default.mak | 1 + hw/riscv/Kconfig | 11 + hw/riscv/meson.build | 1 + hw/riscv/spacemit-k3.c | 509 ++++++++++++++++++++ include/hw/riscv/spacemit-k3.h | 69 +++ 6 files changed, 598 insertions(+) create mode 100644 hw/riscv/spacemit-k3.c create mode 100644 include/hw/riscv/spacemit-k3.h diff --git a/MAINTAINERS b/MAINTAINERS index a6e8d36468243..178a73dcbde72 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1814,6 +1814,13 @@ F: include/hw/watchdog/k230_wdt.h F: tests/qtest/k230-sysctl-test.c F: tests/qtest/k230-wdt-test.c +SpacemiT K3 Pico-ITX +M: Chao Liu +L: qemu-riscv@nongnu.org +S: Maintained +F: hw/riscv/spacemit-k3.c +F: include/hw/riscv/spacemit-k3.h + Milk-V Duo M: Kuan-Wei Chiu S: Maintained diff --git a/configs/devices/riscv64-softmmu/default.mak b/configs/devices/riscv64-softmmu/default.mak index 5af71e5b56282..6d93cfeb0ee37 100644 --- a/configs/devices/riscv64-softmmu/default.mak +++ b/configs/devices/riscv64-softmmu/default.mak @@ -10,6 +10,7 @@ # CONFIG_SIFIVE_U=n # CONFIG_RISCV_VIRT=n # CONFIG_RISCV_SERVER_PLATFORM_REF=n +# CONFIG_SPACEMIT_K3=n # CONFIG_MICROCHIP_PFSOC=n # CONFIG_SHAKTI_C=n # CONFIG_K230=n diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig index 070418d78919a..8365ce39ee9d4 100644 --- a/hw/riscv/Kconfig +++ b/hw/riscv/Kconfig @@ -85,6 +85,17 @@ config RISCV_SERVER_PLATFORM_REF select RISCV_IMSIC select RISCV_IOMMU +config SPACEMIT_K3 + bool + default y + depends on RISCV64 + select CPU_CLUSTER + select DEVICE_TREE + select RISCV_ACLINT + select RISCV_APLIC + select RISCV_IMSIC + select SERIAL_MM + config SHAKTI_C bool default y diff --git a/hw/riscv/meson.build b/hw/riscv/meson.build index 5319b0d0336a2..c10eda9c7bd95 100644 --- a/hw/riscv/meson.build +++ b/hw/riscv/meson.build @@ -5,6 +5,7 @@ riscv_ss.add(files('riscv_hart.c')) riscv_ss.add(when: 'CONFIG_OPENTITAN', if_true: files('opentitan.c')) riscv_ss.add(when: 'CONFIG_RISCV_VIRT', if_true: files('virt.c')) riscv_ss.add(when: 'CONFIG_RISCV_SERVER_PLATFORM_REF', if_true: files('server_platform_ref.c')) +riscv_ss.add(when: 'CONFIG_SPACEMIT_K3', if_true: files('spacemit-k3.c')) riscv_ss.add(when: 'CONFIG_SHAKTI_C', if_true: files('shakti_c.c')) riscv_ss.add(when: 'CONFIG_SIFIVE_E', if_true: files('sifive_e.c')) riscv_ss.add(when: 'CONFIG_SIFIVE_U', if_true: files('sifive_u.c')) diff --git a/hw/riscv/spacemit-k3.c b/hw/riscv/spacemit-k3.c new file mode 100644 index 0000000000000..5535225c39f2b --- /dev/null +++ b/hw/riscv/spacemit-k3.c @@ -0,0 +1,509 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * SpacemiT K3 Pico-ITX machine + * + * This model implements the standard RISC-V platform subset needed to boot + * the K3 SDK Linux kernel with generic OpenSBI. K3 vendor firmware and the + * A100/IME harts are intentionally outside this machine's current contract. + */ + +#include "qemu/osdep.h" +#include "qemu/units.h" +#include "qapi/error.h" +#include "qemu/error-report.h" +#include "hw/core/loader.h" +#include "hw/core/qdev-properties.h" +#include "hw/intc/riscv_aclint.h" +#include "hw/intc/riscv_aplic.h" +#include "hw/intc/riscv_imsic.h" +#include "hw/riscv/boot.h" +#include "hw/riscv/machines-qom.h" +#include "hw/riscv/spacemit-k3.h" +#include "system/address-spaces.h" +#include "system/device_tree.h" +#include "system/qtest.h" +#include "system/system.h" +#include "target/riscv/cpu.h" +#include "target/riscv/cpu-qom.h" +#include "target/riscv/time_helper.h" + +#include + +const MemMapEntry spacemit_k3_memmap[] = { + [K3_DEV_RESET] = { 0xc0800000, 0x1000 }, + [K3_DEV_UART0] = { 0xd4017000, 0x100 }, + [K3_DEV_S_IMSIC] = { 0xe0400000, 0x400000 }, + [K3_DEV_S_APLIC] = { 0xe0804000, 0x4000 }, + [K3_DEV_M_IMSIC] = { 0xf1000000, 0x10000 }, + [K3_DEV_M_APLIC] = { 0xf1800000, 0x4000 }, + [K3_DEV_M_CLINT] = { 0xf1810000, 0x10000 }, + [K3_DEV_FIRMWARE] = { 0x100000000, 32 * MiB }, + [K3_DEV_DRAM] = { 0x102000000, 0 }, +}; + +static RISCVCPU *k3_pico_itx_hart(SpacemitK3SoCState *s, unsigned int hartid) +{ + unsigned int cluster = hartid / K3_PICO_ITX_HARTS_PER_CLUSTER; + unsigned int index = hartid % K3_PICO_ITX_HARTS_PER_CLUSTER; + + return &s->cpus[cluster].harts[index]; +} + +static bool k3_pico_itx_validate_cpu(SpacemitK3SoCState *s, Error **errp) +{ + unsigned int hartid; + + for (hartid = 0; hartid < K3_PICO_ITX_NUM_HARTS; hartid++) { + RISCVCPU *cpu = k3_pico_itx_hart(s, hartid); + CPUState *cs = CPU(cpu); + unsigned int expected_cluster = + hartid / K3_PICO_ITX_HARTS_PER_CLUSTER; + + if (!riscv_has_ext(&cpu->env, RVV)) { + error_setg(errp, + "K3 X100 hart %u requires the V extension", + hartid); + return false; + } + if (cpu->cfg.vlenb != 256 / 8) { + error_setg(errp, "K3 X100 hart %u requires VLEN=256", hartid); + return false; + } + if (!cpu->cfg.ext_smaia || !cpu->cfg.ext_ssaia) { + error_setg(errp, + "K3 X100 hart %u requires Smaia and Ssaia", hartid); + return false; + } + if (!cpu->cfg.ext_sstc) { + error_setg(errp, "K3 X100 hart %u requires Sstc", hartid); + return false; + } + if (riscv_has_ext(&cpu->env, RVH)) { + error_setg(errp, + "K3 Linux-first X100 hart %u does not support H", + hartid); + return false; + } + if (cs->cluster_index != expected_cluster) { + error_setg(errp, + "K3 hart %u has cluster index %d, expected %u", + hartid, cs->cluster_index, expected_cluster); + return false; + } + } + + return true; +} + +/* + * The RISC-V qtest accelerator does not install TCG's profile and feature + * properties. Supply the subset exercised by this non-executing accelerator + * directly; TCG still gets the same values through machine compatibility + * properties and is checked below. + */ +static void k3_pico_itx_apply_qtest_cpu_defaults(SpacemitK3SoCState *s) +{ + unsigned int hartid; + + if (!qtest_enabled()) { + return; + } + + for (hartid = 0; hartid < K3_PICO_ITX_NUM_HARTS; hartid++) { + RISCVCPU *cpu = k3_pico_itx_hart(s, hartid); + + cpu->env.misa_ext |= RVS | RVU | RVV; + cpu->env.misa_ext &= ~RVH; + cpu->env.priv_ver = PRIV_VERSION_1_13_0; + cpu->cfg.vlenb = 256 / 8; + cpu->cfg.ext_zicsr = true; + cpu->cfg.ext_smaia = true; + cpu->cfg.ext_ssaia = true; + cpu->cfg.ext_sstc = true; + riscv_timer_init(cpu); + } +} + +static void k3_pico_itx_create_aia(SpacemitK3SoCState *s) +{ + unsigned int hartid; + + for (hartid = 0; hartid < K3_PICO_ITX_NUM_HARTS; hartid++) { + s->m_imsic[hartid] = riscv_imsic_create( + spacemit_k3_memmap[K3_DEV_M_IMSIC].base + + hartid * IMSIC_HART_SIZE(0), + hartid, true, 1, K3_PICO_ITX_IMSIC_NUM_IDS); + } + + /* + * K3 assigns a 0x40000-byte interrupt-file stride to each X100 hart. + * Only the 511-ID S-mode page is modeled. The VS pages in each stride + * and the aperture for A100 harts remain unmapped. + */ + for (hartid = 0; hartid < K3_PICO_ITX_NUM_HARTS; hartid++) { + s->s_imsic[hartid] = riscv_imsic_create( + spacemit_k3_memmap[K3_DEV_S_IMSIC].base + hartid * 0x40000, + hartid, false, 1, K3_PICO_ITX_IMSIC_NUM_IDS); + } + + /* MSI-mode APLICs have no direct per-hart interrupt contexts. */ + s->m_aplic = riscv_aplic_create( + spacemit_k3_memmap[K3_DEV_M_APLIC].base, + spacemit_k3_memmap[K3_DEV_M_APLIC].size, + 0, 0, K3_PICO_ITX_APLIC_NUM_SOURCES, + K3_PICO_ITX_APLIC_IPRIO_BITS, true, true, NULL); + s->s_aplic = riscv_aplic_create( + spacemit_k3_memmap[K3_DEV_S_APLIC].base, + spacemit_k3_memmap[K3_DEV_S_APLIC].size, + 0, 0, K3_PICO_ITX_APLIC_NUM_SOURCES, + K3_PICO_ITX_APLIC_IPRIO_BITS, true, false, s->m_aplic); +} + +static void spacemit_k3_soc_realize(DeviceState *dev, Error **errp) +{ + SpacemitK3SoCState *s = SPACEMIT_K3_SOC(dev); + MachineState *ms = MACHINE(qdev_get_machine()); + MemoryRegion *system_memory = get_system_memory(); + unsigned int cluster; + + memory_region_init_rom(&s->reset, OBJECT(dev), "spacemit.k3.reset", + spacemit_k3_memmap[K3_DEV_RESET].size, errp); + if (*errp) { + return; + } + memory_region_add_subregion(system_memory, + spacemit_k3_memmap[K3_DEV_RESET].base, + &s->reset); + + memory_region_init_ram(&s->firmware, OBJECT(dev), "spacemit.k3.firmware", + spacemit_k3_memmap[K3_DEV_FIRMWARE].size, errp); + if (*errp) { + return; + } + memory_region_add_subregion(system_memory, + spacemit_k3_memmap[K3_DEV_FIRMWARE].base, + &s->firmware); + + for (cluster = 0; cluster < K3_PICO_ITX_NUM_CLUSTERS; cluster++) { + qdev_prop_set_string(DEVICE(&s->cpus[cluster]), "cpu-type", + ms->cpu_type); + if (!sysbus_realize(SYS_BUS_DEVICE(&s->cpus[cluster]), errp)) { + return; + } + } + + /* The CPUs must exist below each cluster before the cluster is realized. */ + for (cluster = 0; cluster < K3_PICO_ITX_NUM_CLUSTERS; cluster++) { + if (!qdev_realize(DEVICE(&s->clusters[cluster]), NULL, errp)) { + return; + } + } + + k3_pico_itx_apply_qtest_cpu_defaults(s); + if (!k3_pico_itx_validate_cpu(s, errp)) { + return; + } + + s->swi = riscv_aclint_swi_create( + spacemit_k3_memmap[K3_DEV_M_CLINT].base, + 0, K3_PICO_ITX_NUM_HARTS, false); + s->mtimer = riscv_aclint_mtimer_create( + spacemit_k3_memmap[K3_DEV_M_CLINT].base + RISCV_ACLINT_SWI_SIZE, + RISCV_ACLINT_DEFAULT_MTIMER_SIZE, + 0, K3_PICO_ITX_NUM_HARTS, + RISCV_ACLINT_DEFAULT_MTIMECMP, RISCV_ACLINT_DEFAULT_MTIME, + K3_PICO_ITX_TIMEBASE_FREQ, true); + + k3_pico_itx_create_aia(s); + + memory_region_init(&s->uart0_mem, OBJECT(dev), "spacemit.k3.uart0", + spacemit_k3_memmap[K3_DEV_UART0].size); + memory_region_add_subregion(system_memory, + spacemit_k3_memmap[K3_DEV_UART0].base, + &s->uart0_mem); + s->uart0 = serial_mm_init( + &s->uart0_mem, 0, 2, + qdev_get_gpio_in(s->m_aplic, K3_PICO_ITX_UART0_IRQ), + 115200, serial_hd(0), DEVICE_LITTLE_ENDIAN); +} + +static void spacemit_k3_soc_instance_init(Object *obj) +{ + SpacemitK3SoCState *s = SPACEMIT_K3_SOC(obj); + unsigned int cluster; + + object_initialize_child(obj, "cluster0", &s->clusters[0], + TYPE_CPU_CLUSTER); + object_initialize_child(OBJECT(&s->clusters[0]), "cpus", &s->cpus[0], + TYPE_RISCV_HART_ARRAY); + object_initialize_child(obj, "cluster1", &s->clusters[1], + TYPE_CPU_CLUSTER); + object_initialize_child(OBJECT(&s->clusters[1]), "cpus", &s->cpus[1], + TYPE_RISCV_HART_ARRAY); + + for (cluster = 0; cluster < K3_PICO_ITX_NUM_CLUSTERS; cluster++) { + qdev_prop_set_uint32(DEVICE(&s->clusters[cluster]), "cluster-id", + cluster); + qdev_prop_set_uint32(DEVICE(&s->cpus[cluster]), "num-harts", + K3_PICO_ITX_HARTS_PER_CLUSTER); + qdev_prop_set_uint32(DEVICE(&s->cpus[cluster]), "hartid-base", + cluster * K3_PICO_ITX_HARTS_PER_CLUSTER); + qdev_prop_set_uint64(DEVICE(&s->cpus[cluster]), "resetvec", + spacemit_k3_memmap[K3_DEV_RESET].base); + } +} + +static void spacemit_k3_soc_class_init(ObjectClass *oc, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(oc); + + dc->realize = spacemit_k3_soc_realize; + dc->user_creatable = false; +} + +static const TypeInfo spacemit_k3_soc_type_info = { + .name = TYPE_SPACEMIT_K3_SOC, + .parent = TYPE_DEVICE, + .instance_size = sizeof(SpacemitK3SoCState), + .instance_init = spacemit_k3_soc_instance_init, + .class_init = spacemit_k3_soc_class_init, +}; + +static void k3_pico_itx_validate_machine(MachineState *machine) +{ + const CpuTopology *smp = &machine->smp; + + if (machine->ram_size != 2 * GiB) { + error_report("k3-pico-itx requires exactly 2 GiB of RAM"); + exit(EXIT_FAILURE); + } + if (strcmp(machine->cpu_type, TYPE_RISCV_CPU_SPACEMIT_X100)) { + error_report("k3-pico-itx requires the spacemit-x100 CPU type"); + exit(EXIT_FAILURE); + } + if (smp->cpus != K3_PICO_ITX_NUM_HARTS || + smp->max_cpus != K3_PICO_ITX_NUM_HARTS || + smp->sockets != 1 || smp->clusters != K3_PICO_ITX_NUM_CLUSTERS || + smp->cores != K3_PICO_ITX_HARTS_PER_CLUSTER || smp->threads != 1) { + error_report("k3-pico-itx requires " + "cpus=8,sockets=1,clusters=2,cores=4,threads=1,maxcpus=8"); + exit(EXIT_FAILURE); + } +} + +static void k3_pico_itx_validate_fdt(K3PicoITXState *s) +{ + MachineState *machine = MACHINE(s); + const char expected_compat[] = "spacemit,k3-pico-itx"; + const fdt32_t *prop; + int len; + int offset; + unsigned int hartid; + uint64_t base; + uint64_t size; + + if (fdt_node_check_compatible(machine->fdt, 0, expected_compat)) { + error_report("k3-pico-itx DTB requires root compatible '%s'", + expected_compat); + exit(EXIT_FAILURE); + } + + offset = fdt_path_offset(machine->fdt, "/cpus"); + prop = offset >= 0 ? + fdt_getprop(machine->fdt, offset, "timebase-frequency", &len) : NULL; + if (!prop || len != sizeof(*prop) || + fdt32_to_cpu(*prop) != K3_PICO_ITX_TIMEBASE_FREQ) { + error_report("k3-pico-itx DTB requires a 24 MHz timebase"); + exit(EXIT_FAILURE); + } + + for (hartid = 0; hartid < K3_PICO_ITX_NUM_HARTS; hartid++) { + g_autofree char *node = g_strdup_printf("/cpus/cpu@%x", hartid); + + if (fdt_path_offset(machine->fdt, node) < 0) { + error_report("k3-pico-itx DTB is missing X100 hart %u", hartid); + exit(EXIT_FAILURE); + } + riscv_isa_write_fdt(k3_pico_itx_hart(&s->soc, hartid), + machine->fdt, node); + } + if (fdt_path_offset(machine->fdt, "/cpus/cpu@8") >= 0) { + error_report("k3-pico-itx Linux-first DTB must not expose A100 harts"); + exit(EXIT_FAILURE); + } + + offset = fdt_path_offset(machine->fdt, "/memory@102000000"); + prop = offset >= 0 ? fdt_getprop(machine->fdt, offset, "reg", &len) : NULL; + if (!prop || len != 4 * sizeof(*prop)) { + error_report("k3-pico-itx DTB requires the 64-bit Linux memory node"); + exit(EXIT_FAILURE); + } + base = ((uint64_t)fdt32_to_cpu(prop[0]) << 32) | fdt32_to_cpu(prop[1]); + size = ((uint64_t)fdt32_to_cpu(prop[2]) << 32) | fdt32_to_cpu(prop[3]); + if (base != spacemit_k3_memmap[K3_DEV_DRAM].base || + size != machine->ram_size) { + error_report("k3-pico-itx DTB memory must be 2 GiB at 0x102000000"); + exit(EXIT_FAILURE); + } + + if (fdt_path_offset(machine->fdt, "/chosen") < 0) { + qemu_fdt_add_subnode(machine->fdt, "/chosen"); + } +} + +static void k3_pico_itx_check_loaded_images(MachineState *machine, + RISCVBootInfo *info) +{ + hwaddr dram_base = spacemit_k3_memmap[K3_DEV_DRAM].base; + hwaddr dram_end = dram_base + machine->ram_size; + + if (info->kernel_size && + (info->image_low_addr < dram_base || + info->image_high_addr > dram_end || + info->image_high_addr < info->image_low_addr)) { + error_report("K3 Linux kernel does not fit in the Linux RAM window"); + exit(EXIT_FAILURE); + } + if (info->initrd_size && + (info->initrd_start < dram_base || + info->initrd_start + info->initrd_size > dram_end || + info->initrd_start + info->initrd_size < info->initrd_start)) { + error_report("K3 initramfs does not fit in the Linux RAM window"); + exit(EXIT_FAILURE); + } +} + +static void k3_pico_itx_machine_init(MachineState *machine) +{ + K3PicoITXState *s = K3_PICO_ITX_MACHINE(machine); + MemoryRegion *system_memory = get_system_memory(); + RISCVBootInfo boot_info; + hwaddr firmware_load_addr = spacemit_k3_memmap[K3_DEV_FIRMWARE].base; + hwaddr firmware_end_addr = firmware_load_addr; + hwaddr fdt_load_addr = 0; + uint64_t kernel_entry = 0; + + k3_pico_itx_validate_machine(machine); + + memory_region_add_subregion(system_memory, + spacemit_k3_memmap[K3_DEV_DRAM].base, + machine->ram); + qdev_realize(DEVICE(&s->soc), NULL, &error_fatal); + + riscv_boot_info_init(&boot_info, &s->soc.cpus[0]); + + if (machine->kernel_filename && + machine->firmware && !strcmp(machine->firmware, "none")) { + error_report("k3-pico-itx direct Linux boot requires OpenSBI firmware"); + exit(EXIT_FAILURE); + } + + firmware_end_addr = riscv_find_and_load_firmware( + machine, riscv_default_firmware_name(&s->soc.cpus[0]), + &firmware_load_addr, NULL); + if (firmware_load_addr < spacemit_k3_memmap[K3_DEV_FIRMWARE].base || + firmware_end_addr > spacemit_k3_memmap[K3_DEV_DRAM].base || + firmware_end_addr < firmware_load_addr) { + error_report("K3 firmware must fit in 0x100000000..0x102000000"); + exit(EXIT_FAILURE); + } + + if (machine->kernel_filename && !machine->dtb) { + error_report("k3-pico-itx direct Linux boot requires -dtb"); + exit(EXIT_FAILURE); + } + if (machine->dtb) { + machine->fdt = load_device_tree(machine->dtb, &s->fdt_size); + if (!machine->fdt) { + error_report("failed to load k3-pico-itx device tree"); + exit(EXIT_FAILURE); + } + k3_pico_itx_validate_fdt(s); + } + + if (machine->kernel_filename) { + hwaddr kernel_start_addr = MAX( + riscv_calc_kernel_start_addr(&boot_info, firmware_end_addr), + spacemit_k3_memmap[K3_DEV_DRAM].base); + + riscv_load_kernel(machine, &boot_info, kernel_start_addr, true, NULL); + k3_pico_itx_check_loaded_images(machine, &boot_info); + kernel_entry = boot_info.image_low_addr; + } + + if (machine->fdt) { + fdt_load_addr = riscv_compute_fdt_addr( + spacemit_k3_memmap[K3_DEV_DRAM].base, machine->ram_size, + machine, &boot_info); + if (fdt_load_addr < spacemit_k3_memmap[K3_DEV_DRAM].base || + fdt_load_addr + fdt_totalsize(machine->fdt) > + spacemit_k3_memmap[K3_DEV_DRAM].base + machine->ram_size) { + error_report("K3 DTB does not fit in the Linux RAM window"); + exit(EXIT_FAILURE); + } + riscv_load_fdt(fdt_load_addr, machine->fdt); + } + + riscv_setup_rom_reset_vec(machine, &s->soc.cpus[0], firmware_load_addr, + spacemit_k3_memmap[K3_DEV_RESET].base, + spacemit_k3_memmap[K3_DEV_RESET].size, + kernel_entry, fdt_load_addr); +} + +static void k3_pico_itx_machine_instance_init(Object *obj) +{ + K3PicoITXState *s = K3_PICO_ITX_MACHINE(obj); + MachineState *machine = MACHINE(obj); + + machine->smp.cpus = K3_PICO_ITX_NUM_HARTS; + machine->smp.max_cpus = K3_PICO_ITX_NUM_HARTS; + machine->smp.sockets = 1; + machine->smp.clusters = K3_PICO_ITX_NUM_CLUSTERS; + machine->smp.cores = K3_PICO_ITX_HARTS_PER_CLUSTER; + machine->smp.threads = 1; + + object_initialize_child(obj, "soc", &s->soc, TYPE_SPACEMIT_K3_SOC); +} + +static GlobalProperty k3_pico_itx_cpu_defaults[] = { + { TYPE_RISCV_CPU_SPACEMIT_X100, "h", "false", .optional = true }, +}; + +static void k3_pico_itx_machine_class_init(ObjectClass *oc, const void *data) +{ + MachineClass *mc = MACHINE_CLASS(oc); + static const char *const valid_cpu_types[] = { + TYPE_RISCV_CPU_SPACEMIT_X100, + NULL, + }; + + mc->desc = "SpacemiT K3 Pico-ITX (Linux-first X100 subset)"; + mc->init = k3_pico_itx_machine_init; + mc->min_cpus = K3_PICO_ITX_NUM_HARTS; + mc->max_cpus = K3_PICO_ITX_NUM_HARTS; + mc->default_cpus = K3_PICO_ITX_NUM_HARTS; + mc->default_cpu_type = TYPE_RISCV_CPU_SPACEMIT_X100; + mc->valid_cpu_types = valid_cpu_types; + mc->default_ram_size = 2 * GiB; + mc->default_ram_id = "spacemit.k3.ram"; + mc->smp_props.clusters_supported = true; + compat_props_add(mc->compat_props, k3_pico_itx_cpu_defaults, + G_N_ELEMENTS(k3_pico_itx_cpu_defaults)); +} + +static const TypeInfo k3_pico_itx_machine_type_info = { + .name = TYPE_K3_PICO_ITX_MACHINE, + .parent = TYPE_MACHINE, + .instance_size = sizeof(K3PicoITXState), + .instance_init = k3_pico_itx_machine_instance_init, + .class_init = k3_pico_itx_machine_class_init, + .interfaces = riscv64_machine_interfaces, +}; + +static void spacemit_k3_register_types(void) +{ + type_register_static(&spacemit_k3_soc_type_info); + type_register_static(&k3_pico_itx_machine_type_info); +} + +type_init(spacemit_k3_register_types) diff --git a/include/hw/riscv/spacemit-k3.h b/include/hw/riscv/spacemit-k3.h new file mode 100644 index 0000000000000..f6bb1e76adb4f --- /dev/null +++ b/include/hw/riscv/spacemit-k3.h @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * SpacemiT K3 SoC and Pico-ITX machine + */ + +#ifndef HW_SPACEMIT_K3_H +#define HW_SPACEMIT_K3_H + +#include "exec/hwaddr.h" +#include "hw/char/serial-mm.h" +#include "hw/core/boards.h" +#include "hw/cpu/cluster.h" +#include "hw/riscv/riscv_hart.h" + +#define K3_PICO_ITX_NUM_CLUSTERS 2 +#define K3_PICO_ITX_HARTS_PER_CLUSTER 4 +#define K3_PICO_ITX_NUM_HARTS 8 + +#define K3_PICO_ITX_TIMEBASE_FREQ 24000000 +#define K3_PICO_ITX_APLIC_NUM_SOURCES 512 +#define K3_PICO_ITX_APLIC_IPRIO_BITS 8 +#define K3_PICO_ITX_IMSIC_NUM_IDS 511 +#define K3_PICO_ITX_UART0_IRQ 42 + +enum { + K3_DEV_RESET, + K3_DEV_UART0, + K3_DEV_S_IMSIC, + K3_DEV_S_APLIC, + K3_DEV_M_IMSIC, + K3_DEV_M_APLIC, + K3_DEV_M_CLINT, + K3_DEV_FIRMWARE, + K3_DEV_DRAM, +}; + +extern const MemMapEntry spacemit_k3_memmap[]; + +#define TYPE_SPACEMIT_K3_SOC "spacemit.k3.soc" +OBJECT_DECLARE_SIMPLE_TYPE(SpacemitK3SoCState, SPACEMIT_K3_SOC) + +struct SpacemitK3SoCState { + DeviceState parent_obj; + + CPUClusterState clusters[K3_PICO_ITX_NUM_CLUSTERS]; + RISCVHartArrayState cpus[K3_PICO_ITX_NUM_CLUSTERS]; + MemoryRegion reset; + MemoryRegion firmware; + MemoryRegion uart0_mem; + DeviceState *m_imsic[K3_PICO_ITX_NUM_HARTS]; + DeviceState *s_imsic[K3_PICO_ITX_NUM_HARTS]; + DeviceState *m_aplic; + DeviceState *s_aplic; + DeviceState *swi; + DeviceState *mtimer; + SerialMM *uart0; +}; + +#define TYPE_K3_PICO_ITX_MACHINE MACHINE_TYPE_NAME("k3-pico-itx") +OBJECT_DECLARE_SIMPLE_TYPE(K3PicoITXState, K3_PICO_ITX_MACHINE) + +struct K3PicoITXState { + MachineState parent_obj; + + SpacemitK3SoCState soc; + int fdt_size; +}; + +#endif From b5c604825cd0241b595fe82933402e304ac3e0a5 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Tue, 14 Jul 2026 06:43:05 +0800 Subject: [PATCH 03/13] tests/qtest: add SpacemiT K3 machine tests Cover the fixed spacemit-x100 CPU type and topology, 256-bit VLEN, firmware and DRAM windows, 24 MHz timer and Sstc behavior, per-hart M/S IMSIC routing, and the UART0 interrupt path through the M/S APLIC hierarchy. Signed-off-by: Chao Liu --- MAINTAINERS | 1 + tests/qtest/meson.build | 3 + tests/qtest/spacemit-k3-test.c | 502 +++++++++++++++++++++++++++++++++ 3 files changed, 506 insertions(+) create mode 100644 tests/qtest/spacemit-k3-test.c diff --git a/MAINTAINERS b/MAINTAINERS index 178a73dcbde72..89f3d4978efc7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1820,6 +1820,7 @@ L: qemu-riscv@nongnu.org S: Maintained F: hw/riscv/spacemit-k3.c F: include/hw/riscv/spacemit-k3.h +F: tests/qtest/spacemit-k3-test.c Milk-V Duo M: Kuan-Wei Chiu diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build index 750c8ea1d2267..48359142c3cff 100644 --- a/tests/qtest/meson.build +++ b/tests/qtest/meson.build @@ -292,6 +292,8 @@ qtests_riscv32 = \ qtests_riscv64 = ['riscv-csr-test'] + \ (unpack_edk2_blobs ? ['bios-tables-test'] : []) + \ + (config_all_devices.has_key('CONFIG_SPACEMIT_K3') ? + ['spacemit-k3-test'] : []) + \ (config_all_devices.has_key('CONFIG_IOMMU_TESTDEV') and config_all_devices.has_key('CONFIG_RISCV_IOMMU') ? ['iommu-riscv-test'] : []) + \ @@ -407,6 +409,7 @@ qtests = { 'pnv-xive2-test': files('pnv-xive2-common.c', 'pnv-xive2-flush-sync.c', 'pnv-xive2-nvpg_bar.c'), 'qos-test': [chardev, io, qos_test_ss.apply({}).sources()], + 'spacemit-k3-test': [fdt], 'tpm-crb-swtpm-test': [io, tpmemu_files], 'tpm-crb-test': [io, tpmemu_files], 'tpm-tis-swtpm-test': [io, tpmemu_files, 'tpm-tis-util.c'], diff --git a/tests/qtest/spacemit-k3-test.c b/tests/qtest/spacemit-k3-test.c new file mode 100644 index 0000000000000..1536868d2592c --- /dev/null +++ b/tests/qtest/spacemit-k3-test.c @@ -0,0 +1,502 @@ +/* + * QTest testcase for the SpacemiT K3 Pico-ITX machine + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include +#include +#include "qemu/bswap.h" +#include "qemu/units.h" +#include "libqtest.h" +#include "qobject/qdict.h" +#include "qobject/qlist.h" + +#define K3_NUM_HARTS 8 +#define K3_HARTS_PER_CLUSTER 4 +#define K3_TIMEBASE_FREQ 24000000 + +#define K3_RESET_BASE UINT64_C(0xc0800000) +#define K3_RESET_SIZE UINT64_C(0x1000) +#define K3_UART0_BASE UINT64_C(0xd4017000) +#define K3_UART0_SIZE UINT64_C(0x100) +#define K3_S_IMSIC_BASE UINT64_C(0xe0400000) +#define K3_S_IMSIC_HART_STRIDE UINT64_C(0x40000) +#define K3_S_APLIC_BASE UINT64_C(0xe0804000) +#define K3_M_IMSIC_BASE UINT64_C(0xf1000000) +#define K3_M_IMSIC_HART_STRIDE UINT64_C(0x1000) +#define K3_M_APLIC_BASE UINT64_C(0xf1800000) +#define K3_M_CLINT_BASE UINT64_C(0xf1810000) +#define K3_FIRMWARE_BASE UINT64_C(0x100000000) +#define K3_FIRMWARE_SIZE UINT64_C(0x2000000) +#define K3_DRAM_BASE UINT64_C(0x102000000) +#define K3_DRAM_SIZE UINT64_C(0x80000000) + +#define ACLINT_MTIME UINT64_C(0xbff8) + +#define APLIC_DOMAINCFG 0x0000 +#define APLIC_DOMAINCFG_IE (1U << 8) +#define APLIC_DOMAINCFG_DM (1U << 2) +#define APLIC_DOMAINCFG_RDONLY (1U << 31) +#define APLIC_SOURCECFG_BASE 0x0004 +#define APLIC_SOURCECFG_D (1U << 10) +#define APLIC_SOURCECFG_LEVEL_HIGH 0x6 +#define APLIC_MMSICFGADDR 0x1bc0 +#define APLIC_MMSICFGADDRH 0x1bc4 +#define APLIC_SMSICFGADDR 0x1bc8 +#define APLIC_SMSICFGADDRH 0x1bcc +#define APLIC_CLRIP_BASE 0x1d00 +#define APLIC_SETIENUM 0x1edc +#define APLIC_TARGET_BASE 0x3004 +#define APLIC_LHXS_SHIFT 20 +#define APLIC_LHXW_SHIFT 12 + +#define K3_UART0_IRQ 42 +#define UART_RBR 0x00 +#define UART_IER 0x04 +#define UART_LSR 0x14 +#define UART_IER_RDI 0x01 +#define UART_LSR_DR 0x01 + +#define K3_FDT_MAX_SIZE (64 * KiB) + +#define CSR_MIP 0x344 +#define CSR_MENVCFG 0x30a +#define CSR_MISELECT 0x350 +#define CSR_MIREG 0x351 +#define CSR_MTOPEI 0x35c +#define CSR_SISELECT 0x150 +#define CSR_SIREG 0x151 +#define CSR_STOPEI 0x15c +#define CSR_STIMECMP 0x14d +#define MIP_STIP (UINT64_C(1) << 5) +#define MIP_SEIP (UINT64_C(1) << 9) +#define MIP_MEIP (UINT64_C(1) << 11) +#define MENVCFG_STCE (UINT64_C(1) << 63) +#define ISELECT_IMSIC_EIDELIVERY 0x70 +#define ISELECT_IMSIC_EIP0 0x80 +#define ISELECT_IMSIC_EIE0 0xc0 + +static QTestState *k3_qtest_init(void) +{ + return qtest_init("-M k3-pico-itx -bios none -display none " + "-nodefaults"); +} + +static char *k3_create_fdt(const void *compatible, size_t compatible_len) +{ + g_autofree uint8_t *fdt = g_malloc0(K3_FDT_MAX_SIZE); + const fdt32_t memory_reg[] = { + cpu_to_fdt32((uint32_t)(K3_DRAM_BASE >> 32)), + cpu_to_fdt32((uint32_t)K3_DRAM_BASE), + cpu_to_fdt32((uint32_t)(K3_DRAM_SIZE >> 32)), + cpu_to_fdt32((uint32_t)K3_DRAM_SIZE), + }; + g_autoptr(GError) error = NULL; + char *path = g_strdup("spacemit-k3-fdt-XXXXXX"); + int cpus; + int memory; + int fd; + + g_assert_cmpint(fdt_create_empty_tree(fdt, K3_FDT_MAX_SIZE), ==, 0); + g_assert_cmpint(fdt_setprop(fdt, 0, "compatible", compatible, + compatible_len), ==, 0); + + cpus = fdt_add_subnode(fdt, 0, "cpus"); + g_assert_cmpint(cpus, >=, 0); + g_assert_cmpint(fdt_setprop_u32(fdt, cpus, "timebase-frequency", + K3_TIMEBASE_FREQ), ==, 0); + for (unsigned int i = 0; i < K3_NUM_HARTS; i++) { + g_autofree char *name = g_strdup_printf("cpu@%x", i); + + g_assert_cmpint(fdt_add_subnode(fdt, cpus, name), >=, 0); + } + + memory = fdt_add_subnode(fdt, 0, "memory@102000000"); + g_assert_cmpint(memory, >=, 0); + g_assert_cmpint(fdt_setprop(fdt, memory, "reg", memory_reg, + sizeof(memory_reg)), ==, 0); + g_assert_cmpint(fdt_pack(fdt), ==, 0); + + fd = g_mkstemp(path); + g_assert_cmpint(fd, >=, 0); + close(fd); + g_assert_true(g_file_set_contents(path, (const char *)fdt, + fdt_totalsize(fdt), &error)); + g_assert_no_error(error); + + return path; +} + +static void test_fdt_compatible_list(void) +{ + static const char compatible[] = + "spacemit,k3\0spacemit,k3-pico-itx"; + g_autofree char *dtb_path = k3_create_fdt(compatible, + sizeof(compatible)); + QTestState *qts = qtest_initf( + "-M k3-pico-itx -bios none -display none -nodefaults -dtb %s", + dtb_path); + + qtest_quit(qts); + g_assert_cmpint(g_unlink(dtb_path), ==, 0); +} + +static void test_fdt_missing_compatible(void) +{ + static const char compatible[] = "spacemit,k3"; + g_autofree char *dtb_path = k3_create_fdt(compatible, + sizeof(compatible)); + g_autofree char *stderr_buf = NULL; + g_autoptr(GError) error = NULL; + /* Make an unexpected successful DTB validation terminate promptly. */ + const char *argv[] = { + qtest_qemu_binary(NULL), + "-M", "k3-pico-itx", + "-bios", "none", + "-display", "none", + "-nodefaults", + "-dtb", dtb_path, + "-device", "spacemit-k3-test-invalid", + NULL, + }; + int exit_status; + bool spawned; + + spawned = g_spawn_sync(NULL, (char **)argv, NULL, + G_SPAWN_STDOUT_TO_DEV_NULL, NULL, NULL, NULL, + &stderr_buf, &exit_status, &error); + g_assert_true(spawned); + g_assert_no_error(error); + g_assert_false(g_spawn_check_exit_status(exit_status, NULL)); + g_assert_nonnull(strstr(stderr_buf, + "DTB requires root compatible " + "'spacemit,k3-pico-itx'")); + g_assert_cmpint(g_unlink(dtb_path), ==, 0); +} + +static void test_cpu_requires_vector(void) +{ + g_autofree char *stderr_buf = NULL; + g_autoptr(GError) error = NULL; + const char *argv[] = { + qtest_qemu_binary(NULL), + "-M", "k3-pico-itx", + "-cpu", "spacemit-x100,v=false", + "-bios", "none", + "-display", "none", + "-nodefaults", + "-device", "spacemit-k3-test-invalid", + NULL, + }; + int exit_status; + bool spawned; + + spawned = g_spawn_sync(NULL, (char **)argv, NULL, + G_SPAWN_STDOUT_TO_DEV_NULL, NULL, NULL, NULL, + &stderr_buf, &exit_status, &error); + g_assert_true(spawned); + g_assert_no_error(error); + g_assert_false(g_spawn_check_exit_status(exit_status, NULL)); + g_assert_nonnull(strstr(stderr_buf, + "K3 X100 hart 0 requires the V extension")); +} + +static uint64_t k3_qom_get_uint(QTestState *qts, const char *path, + const char *property) +{ + QDict *response = qtest_qmp(qts, + "{ 'execute': 'qom-get', 'arguments': " + "{ 'path': %s, 'property': %s } }", path, property); + uint64_t value; + + g_assert(qdict_haskey(response, "return")); + value = qdict_get_int(response, "return"); + qobject_unref(response); + + return value; +} + +static uint64_t k3_csr_get(QTestState *qts, unsigned int hartid, int csr) +{ + uint64_t value = 0; + + g_assert_cmpuint(qtest_csr_call(qts, "get_csr", hartid, csr, &value), + ==, 0); + return value; +} + +static void k3_csr_set(QTestState *qts, unsigned int hartid, int csr, + uint64_t value) +{ + g_assert_cmpuint(qtest_csr_call(qts, "set_csr", hartid, csr, &value), + ==, 0); +} + +static uint64_t k3_imsic_indirect_read(QTestState *qts, unsigned int hartid, + bool mmode, uint64_t selector) +{ + k3_csr_set(qts, hartid, mmode ? CSR_MISELECT : CSR_SISELECT, selector); + return k3_csr_get(qts, hartid, mmode ? CSR_MIREG : CSR_SIREG); +} + +static void k3_imsic_indirect_write(QTestState *qts, unsigned int hartid, + bool mmode, uint64_t selector, + uint64_t value) +{ + k3_csr_set(qts, hartid, mmode ? CSR_MISELECT : CSR_SISELECT, selector); + k3_csr_set(qts, hartid, mmode ? CSR_MIREG : CSR_SIREG, value); +} + +static void k3_imsic_enable(QTestState *qts, unsigned int hartid, bool mmode, + unsigned int eiid) +{ + g_assert_cmpuint(eiid, <, 64); + k3_imsic_indirect_write(qts, hartid, mmode, + ISELECT_IMSIC_EIDELIVERY, 1); + k3_imsic_indirect_write(qts, hartid, mmode, ISELECT_IMSIC_EIE0, + UINT64_C(1) << eiid); +} + +static void k3_imsic_claim(QTestState *qts, unsigned int hartid, bool mmode, + unsigned int eiid) +{ + int topei_csr = mmode ? CSR_MTOPEI : CSR_STOPEI; + uint64_t topei = k3_csr_get(qts, hartid, topei_csr); + + g_assert_cmpuint(topei >> 16, ==, eiid); + k3_csr_set(qts, hartid, topei_csr, topei); +} + +static void test_topology(void) +{ + QTestState *qts = k3_qtest_init(); + QDict *response; + QList *cpus; + QListEntry *entry; + bool seen[K3_NUM_HARTS] = { false }; + + response = qtest_qmp(qts, "{ 'execute': 'query-cpus-fast' }"); + g_assert(qdict_haskey(response, "return")); + cpus = qdict_get_qlist(response, "return"); + g_assert_cmpuint(qlist_size(cpus), ==, K3_NUM_HARTS); + + QLIST_FOREACH_ENTRY(cpus, entry) { + QDict *cpu = qobject_to(QDict, entry->value); + unsigned int index = qdict_get_int(cpu, "cpu-index"); + unsigned int cluster = index / K3_HARTS_PER_CLUSTER; + unsigned int hart = index % K3_HARTS_PER_CLUSTER; + g_autofree char *expected_path = g_strdup_printf( + "/machine/soc/cluster%u/cpus/harts[%u]", cluster, hart); + + g_assert_cmpuint(index, <, K3_NUM_HARTS); + g_assert_false(seen[index]); + seen[index] = true; + g_assert_cmpstr(qdict_get_str(cpu, "qom-type"), ==, + "spacemit-x100-riscv-cpu"); + g_assert_cmpstr(qdict_get_str(cpu, "qom-path"), ==, expected_path); + } + for (unsigned int i = 0; i < K3_NUM_HARTS; i++) { + g_assert_true(seen[i]); + } + + g_assert_cmpuint(k3_qom_get_uint(qts, "/machine/soc/cluster0", + "cluster-id"), ==, 0); + g_assert_cmpuint(k3_qom_get_uint(qts, "/machine/soc/cluster1", + "cluster-id"), ==, 1); + g_assert_cmpuint(k3_qom_get_uint( + qts, "/machine/soc/cluster0/cpus/harts[0]", "vlen"), ==, 256); + + qobject_unref(response); + qtest_quit(qts); +} + +static void test_address_map(void) +{ + static const uint64_t first_pattern = UINT64_C(0x0123456789abcdef); + static const uint64_t last_pattern = UINT64_C(0xfedcba9876543210); + QTestState *qts = k3_qtest_init(); + + g_assert_cmphex(qtest_readl(qts, K3_RESET_BASE), ==, 0x00000297); + g_assert_cmphex(qtest_readl(qts, K3_RESET_BASE + K3_RESET_SIZE), ==, 0); + + qtest_writeq(qts, K3_FIRMWARE_BASE, first_pattern); + qtest_writeq(qts, K3_FIRMWARE_BASE + K3_FIRMWARE_SIZE - 8, + last_pattern); + g_assert_cmphex(qtest_readq(qts, K3_FIRMWARE_BASE), ==, first_pattern); + g_assert_cmphex(qtest_readq(qts, + K3_FIRMWARE_BASE + K3_FIRMWARE_SIZE - 8), ==, + last_pattern); + + qtest_writeq(qts, K3_DRAM_BASE, last_pattern); + qtest_writeq(qts, K3_DRAM_BASE + K3_DRAM_SIZE - 8, first_pattern); + g_assert_cmphex(qtest_readq(qts, K3_DRAM_BASE), ==, last_pattern); + g_assert_cmphex(qtest_readq(qts, K3_DRAM_BASE + K3_DRAM_SIZE - 8), ==, + first_pattern); + + g_assert_cmphex(qtest_readl(qts, K3_M_APLIC_BASE + APLIC_DOMAINCFG), ==, + APLIC_DOMAINCFG_RDONLY | APLIC_DOMAINCFG_DM); + g_assert_cmphex(qtest_readl(qts, K3_S_APLIC_BASE + APLIC_DOMAINCFG), ==, + APLIC_DOMAINCFG_RDONLY | APLIC_DOMAINCFG_DM); + g_assert_cmphex(qtest_readl(qts, K3_M_IMSIC_BASE), ==, 0); + g_assert_cmphex(qtest_readl(qts, K3_S_IMSIC_BASE), ==, 0); + g_assert_cmphex(qtest_readl(qts, K3_UART0_BASE + UART_LSR), ==, 0x60); + g_assert_cmphex(qtest_readl(qts, K3_UART0_BASE + K3_UART0_SIZE - 4), ==, + 0); + + qtest_quit(qts); +} + +static void test_timer_and_sstc(void) +{ + QTestState *qts = k3_qtest_init(); + uint64_t before = qtest_readq(qts, K3_M_CLINT_BASE + ACLINT_MTIME); + uint64_t after; + + qtest_clock_step(qts, 1000000); + after = qtest_readq(qts, K3_M_CLINT_BASE + ACLINT_MTIME); + g_assert_cmpuint(after - before, ==, K3_TIMEBASE_FREQ / 1000); + + k3_csr_set(qts, 0, CSR_STIMECMP, UINT64_MAX); + k3_csr_set(qts, 0, CSR_MENVCFG, MENVCFG_STCE); + g_assert_cmphex(k3_csr_get(qts, 0, CSR_MIP) & MIP_STIP, ==, 0); + k3_csr_set(qts, 0, CSR_STIMECMP, after + K3_TIMEBASE_FREQ / 1000); + qtest_clock_step(qts, 999999); + g_assert_cmphex(k3_csr_get(qts, 0, CSR_MIP) & MIP_STIP, ==, 0); + qtest_clock_step(qts, 1); + g_assert_cmphex(k3_csr_get(qts, 0, CSR_MIP) & MIP_STIP, ==, MIP_STIP); + k3_csr_set(qts, 0, CSR_STIMECMP, UINT64_MAX); + g_assert_cmphex(k3_csr_get(qts, 0, CSR_MIP) & MIP_STIP, ==, 0); + + qtest_quit(qts); +} + +static void test_imsic_hart_routing(void) +{ + const unsigned int eiid = 10; + QTestState *qts = k3_qtest_init(); + + k3_imsic_enable(qts, 0, false, eiid); + k3_imsic_enable(qts, 7, false, eiid); + + /* The unimplemented VS page in hart 0's stride must not signal S-mode. */ + qtest_writel(qts, K3_S_IMSIC_BASE + 0x1000, eiid); + g_assert_cmphex(k3_imsic_indirect_read(qts, 0, false, + ISELECT_IMSIC_EIP0), ==, 0); + + qtest_writel(qts, K3_S_IMSIC_BASE, eiid); + g_assert_cmphex(k3_imsic_indirect_read(qts, 0, false, + ISELECT_IMSIC_EIP0), ==, + UINT64_C(1) << eiid); + g_assert_cmphex(k3_csr_get(qts, 0, CSR_MIP) & MIP_SEIP, ==, MIP_SEIP); + g_assert_cmphex(k3_imsic_indirect_read(qts, 7, false, + ISELECT_IMSIC_EIP0), ==, 0); + k3_imsic_claim(qts, 0, false, eiid); + + qtest_writel(qts, K3_S_IMSIC_BASE + 7 * K3_S_IMSIC_HART_STRIDE, eiid); + g_assert_cmphex(k3_imsic_indirect_read(qts, 7, false, + ISELECT_IMSIC_EIP0), ==, + UINT64_C(1) << eiid); + g_assert_cmphex(k3_imsic_indirect_read(qts, 0, false, + ISELECT_IMSIC_EIP0), ==, 0); + k3_imsic_claim(qts, 7, false, eiid); + + k3_imsic_enable(qts, 7, true, eiid); + qtest_writel(qts, K3_M_IMSIC_BASE + 7 * K3_M_IMSIC_HART_STRIDE, eiid); + g_assert_cmphex(k3_imsic_indirect_read(qts, 7, true, + ISELECT_IMSIC_EIP0), ==, + UINT64_C(1) << eiid); + g_assert_cmphex(k3_csr_get(qts, 7, CSR_MIP) & MIP_MEIP, ==, MIP_MEIP); + k3_imsic_claim(qts, 7, true, eiid); + + qtest_quit(qts); +} + +static bool k3_wait_for_uart_rx(QTestState *qts) +{ + for (unsigned int i = 0; i < 10000; i++) { + if (qtest_readl(qts, K3_UART0_BASE + UART_LSR) & UART_LSR_DR) { + return true; + } + g_usleep(100); + } + + return false; +} + +static void test_uart_aplic_imsic(void) +{ + const unsigned int eiid = K3_UART0_IRQ; + const uint64_t sourcecfg = APLIC_SOURCECFG_BASE + + (K3_UART0_IRQ - 1) * 4; + const uint64_t target = APLIC_TARGET_BASE + + (K3_UART0_IRQ - 1) * 4; + const uint64_t input_word = APLIC_CLRIP_BASE + + (K3_UART0_IRQ / 32) * 4; + const uint32_t input_mask = 1U << (K3_UART0_IRQ % 32); + int sock_fd; + QTestState *qts = qtest_init_with_serial( + "-M k3-pico-itx -bios none -display none -nodefaults", &sock_fd); + + k3_imsic_enable(qts, 0, false, eiid); + + qtest_writel(qts, K3_M_APLIC_BASE + APLIC_MMSICFGADDR, + K3_M_IMSIC_BASE >> 12); + qtest_writel(qts, K3_M_APLIC_BASE + APLIC_MMSICFGADDRH, + 3U << APLIC_LHXW_SHIFT); + qtest_writel(qts, K3_M_APLIC_BASE + APLIC_SMSICFGADDR, + K3_S_IMSIC_BASE >> 12); + qtest_writel(qts, K3_M_APLIC_BASE + APLIC_SMSICFGADDRH, + 6U << APLIC_LHXS_SHIFT); + + qtest_writel(qts, K3_M_APLIC_BASE + sourcecfg, APLIC_SOURCECFG_D); + qtest_writel(qts, K3_S_APLIC_BASE + sourcecfg, + APLIC_SOURCECFG_LEVEL_HIGH); + qtest_writel(qts, K3_S_APLIC_BASE + target, eiid); + qtest_writel(qts, K3_S_APLIC_BASE + APLIC_SETIENUM, K3_UART0_IRQ); + qtest_writel(qts, K3_S_APLIC_BASE + APLIC_DOMAINCFG, + APLIC_DOMAINCFG_IE); + + qtest_writel(qts, K3_UART0_BASE + UART_IER, UART_IER_RDI); + g_assert_cmpint(send(sock_fd, "K", 1, 0), ==, 1); + g_assert_true(k3_wait_for_uart_rx(qts)); + + g_assert_cmphex(qtest_readl(qts, K3_S_APLIC_BASE + input_word) & + input_mask, ==, input_mask); + g_assert_cmphex(k3_imsic_indirect_read(qts, 0, false, + ISELECT_IMSIC_EIP0), ==, + UINT64_C(1) << eiid); + g_assert_cmphex(k3_csr_get(qts, 0, CSR_MIP) & MIP_SEIP, ==, MIP_SEIP); + + g_assert_cmphex(qtest_readl(qts, K3_UART0_BASE + UART_RBR), ==, 'K'); + g_assert_cmphex(qtest_readl(qts, K3_S_APLIC_BASE + input_word) & + input_mask, ==, 0); + k3_imsic_claim(qts, 0, false, eiid); + g_assert_cmphex(k3_csr_get(qts, 0, CSR_MIP) & MIP_SEIP, ==, 0); + + close(sock_fd); + qtest_quit(qts); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + + if (qtest_has_machine("k3-pico-itx")) { + qtest_add_func("spacemit-k3/topology", test_topology); + qtest_add_func("spacemit-k3/address-map", test_address_map); + qtest_add_func("spacemit-k3/fdt-compatible-list", + test_fdt_compatible_list); + qtest_add_func("spacemit-k3/fdt-missing-compatible", + test_fdt_missing_compatible); + qtest_add_func("spacemit-k3/cpu-requires-vector", + test_cpu_requires_vector); + qtest_add_func("spacemit-k3/timer-sstc", test_timer_and_sstc); + qtest_add_func("spacemit-k3/imsic-routing", test_imsic_hart_routing); + qtest_add_func("spacemit-k3/uart-aplic-imsic", + test_uart_aplic_imsic); + } + + return g_test_run(); +} From 3652fe1806dc4b02f1629ea76999976c1740eb6e Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Tue, 14 Jul 2026 06:43:33 +0800 Subject: [PATCH 04/13] tests/functional/riscv64: add SpacemiT K3 Linux boot test Boot pinned SDK v1.0.2 artifacts through generic OpenSBI and verify the Linux-visible platform contract, all eight X100 harts, interrupt controllers, UART, and the initramfs completion marker. Use content-addressed assets from the public K3 QEMU image release. Signed-off-by: Chao Liu --- MAINTAINERS | 1 + tests/functional/riscv64/meson.build | 2 + tests/functional/riscv64/test_spacemit_k3.py | 86 ++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100755 tests/functional/riscv64/test_spacemit_k3.py diff --git a/MAINTAINERS b/MAINTAINERS index 89f3d4978efc7..b9f6827c239ce 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1820,6 +1820,7 @@ L: qemu-riscv@nongnu.org S: Maintained F: hw/riscv/spacemit-k3.c F: include/hw/riscv/spacemit-k3.h +F: tests/functional/riscv64/test_spacemit_k3.py F: tests/qtest/spacemit-k3-test.c Milk-V Duo diff --git a/tests/functional/riscv64/meson.build b/tests/functional/riscv64/meson.build index 2eb12586bf0e8..9746f20b3bab4 100644 --- a/tests/functional/riscv64/meson.build +++ b/tests/functional/riscv64/meson.build @@ -3,6 +3,7 @@ test_riscv64_timeouts = { 'boston' : 120, 'server_ref' : 120, + 'spacemit_k3' : 180, 'tuxrun' : 120, } @@ -16,5 +17,6 @@ tests_riscv64_system_thorough = [ 'boston', 'server_ref', 'sifive_u', + 'spacemit_k3', 'tuxrun', ] diff --git a/tests/functional/riscv64/test_spacemit_k3.py b/tests/functional/riscv64/test_spacemit_k3.py new file mode 100755 index 0000000000000..60d75edf280c3 --- /dev/null +++ b/tests/functional/riscv64/test_spacemit_k3.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# Functional test for the SpacemiT K3 Pico-ITX machine +# +# SPDX-License-Identifier: GPL-2.0-or-later + +""" +Boot the pinned SpacemiT K3 SDK Linux image on k3-pico-itx. +""" + +from qemu_test import Asset, QemuSystemTest +from qemu_test import wait_for_console_pattern + + +class SpacemitK3Test(QemuSystemTest): + """Test the Linux-first K3 Pico-ITX boot path.""" + + RELEASE_URL = ( + 'https://github.com/zevorn/spacemit-k3-qemu-images/releases/' + 'download/sdk-v1.0.2-qemu2/' + ) + + ASSET_KERNEL = Asset( + RELEASE_URL + 'Image', + 'cc7359e16ba6569fee49be475d561ab42f90e0aa0e9fb7fc1ead4ab1be066efd') + ASSET_FIRMWARE = Asset( + RELEASE_URL + 'fw_dynamic.bin', + 'e62c515fa5aa5fadee169a316c30f99f365b09ae2eff72c783d936b0b337e0a1') + ASSET_INITRAMFS = Asset( + RELEASE_URL + 'k3-qemu-initramfs.cpio.gz', + '42670a19d756fc239376774577cc267b26fdec872827eed3a02139e8e5ce11e3') + ASSET_DTB = Asset( + RELEASE_URL + 'k3-pico-itx-qemu.dtb', + '35e2fc80a64e41963d091cb23b142ed6dfc1e56f9e808061c6e241d7da4bf56e') + + def _wait_for_linux_boot(self): + panic = 'Kernel panic - not syncing' + expected = ( + 'Linux version 6.18.3-g0ffac20d9ef9', + ('Machine model: SpacemiT K3 Pico-ITX ' + '(QEMU Linux-first subset)'), + 'SBI specification v2.0 detected', + 'SBI implementation ID=0x1 Version=0x10006', + ('riscv-timer: Timer interrupt in S-mode is available ' + 'via sstc extension'), + 'smp: Brought up 1 node, 8 CPUs', + ('riscv-imsic: interrupt-controller@e0400000: ' + 'per-CPU IDs 511'), + ('riscv-aplic e0804000.interrupt-controller: ' + '512 interrupts forwarded'), + 'd4017000.serial: ttyS0 at MMIO 0xd4017000', + ('K3-QEMU: model=SpacemiT K3 Pico-ITX ' + '(QEMU Linux-first subset)'), + 'K3-QEMU: cpus=8', + 'K3-QEMU: Linux boot successful', + 'K3_LINUX_MVP_PASS', + ) + + for pattern in expected: + wait_for_console_pattern(self, pattern, panic) + + def test_linux_boot(self): + self.set_machine('k3-pico-itx') + + kernel_path = self.ASSET_KERNEL.fetch() + firmware_path = self.ASSET_FIRMWARE.fetch() + initramfs_path = self.ASSET_INITRAMFS.fetch() + dtb_path = self.ASSET_DTB.fetch() + + kernel_command_line = ( + 'earlycon=uart8250,mmio32,0xd4017000,115200 ' + 'console=ttyS0,115200 rdinit=/init' + ) + self.vm.add_args('-bios', firmware_path, + '-kernel', kernel_path, + '-initrd', initramfs_path, + '-dtb', dtb_path, + '-append', kernel_command_line, + '-no-reboot') + self.vm.set_console() + self.vm.launch() + self._wait_for_linux_boot() + + +if __name__ == '__main__': + QemuSystemTest.main() From b128ac23c0793fd25bf63744418ef9f45890fa9d Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Tue, 14 Jul 2026 15:16:44 +0800 Subject: [PATCH 05/13] hw/sd: allow ADMA without SDMA capability SDHCI currently clears Transfer Mode DMA unless the controller advertises SDMA. This prevents ADMA-only controllers from using DMA even when ADMA1 or ADMA2 is available. Keep DMA enabled when any implemented DMA engine is advertised. Check the SDMA capability in the transfer dispatcher as well, matching the existing ADMA checks and preventing an ADMA-only controller from selecting SDMA. Signed-off-by: Chao Liu --- hw/sd/sdhci.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/hw/sd/sdhci.c b/hw/sd/sdhci.c index 88a6d05ace4c7..76c94642227f8 100644 --- a/hw/sd/sdhci.c +++ b/hw/sd/sdhci.c @@ -950,6 +950,11 @@ static void sdhci_data_transfer(void *opaque) if (s->trnmod & SDHC_TRNS_DMA) { switch (SDHC_DMA_TYPE(s->hostctl1)) { case SDHC_CTRL_SDMA: + if (!(s->capareg & R_SDHC_CAPAB_SDMA_MASK)) { + trace_sdhci_error("SDMA not supported"); + break; + } + sdhci_sdma_transfer(s); break; case SDHC_CTRL_ADMA1_32: @@ -1248,7 +1253,9 @@ sdhci_write(void *opaque, hwaddr offset, uint64_t val, unsigned size) * DMA can be enabled only if it is supported as indicated by * capabilities register */ - if (!(s->capareg & R_SDHC_CAPAB_SDMA_MASK)) { + if (!(s->capareg & (R_SDHC_CAPAB_SDMA_MASK | + R_SDHC_CAPAB_ADMA1_MASK | + R_SDHC_CAPAB_ADMA2_MASK))) { value &= ~SDHC_TRNS_DMA; } From 23ce9076b4481470ec29e1a9246e1ac6169506c7 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Tue, 14 Jul 2026 15:22:11 +0800 Subject: [PATCH 06/13] hw/misc: add SpacemiT K3 clock and boot controls K3 U-Boot reads the boot source from the chip interface unit and programs the application power management unit while probing SDHCI0. Unmapped accesses stop the boot path before storage initialization. Add narrow CIU and APMU models. Report SD as the boot source and implement the SDH0 reset, gate, mux, divider, and self-clearing frequency-change fields. Log accesses outside the modeled firmware contract instead of providing a catch-all register bank. Signed-off-by: Chao Liu --- MAINTAINERS | 2 + hw/misc/Kconfig | 3 + hw/misc/meson.build | 1 + hw/misc/spacemit-k3.c | 206 ++++++++++++++++++++++++++++++++++ hw/riscv/Kconfig | 1 + include/hw/misc/spacemit-k3.h | 37 ++++++ 6 files changed, 250 insertions(+) create mode 100644 hw/misc/spacemit-k3.c create mode 100644 include/hw/misc/spacemit-k3.h diff --git a/MAINTAINERS b/MAINTAINERS index b9f6827c239ce..91f5beba97ef0 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1818,7 +1818,9 @@ SpacemiT K3 Pico-ITX M: Chao Liu L: qemu-riscv@nongnu.org S: Maintained +F: hw/misc/spacemit-k3.c F: hw/riscv/spacemit-k3.c +F: include/hw/misc/spacemit-k3.h F: include/hw/riscv/spacemit-k3.h F: tests/functional/riscv64/test_spacemit_k3.py F: tests/qtest/spacemit-k3-test.c diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig index ee4d628dbd008..55db0c38ebfd8 100644 --- a/hw/misc/Kconfig +++ b/hw/misc/Kconfig @@ -208,6 +208,9 @@ config RK3588_CRU config RK3588_SCMI bool +config SPACEMIT_K3_CTRL + bool + config MAC_VIA bool select MOS6522 diff --git a/hw/misc/meson.build b/hw/misc/meson.build index 8bb2965b90d4b..e92e19236194c 100644 --- a/hw/misc/meson.build +++ b/hw/misc/meson.build @@ -183,6 +183,7 @@ system_ss.add(when: 'CONFIG_MAC_VIA', if_true: files('mac_via.c')) system_ss.add(when: 'CONFIG_RK3588_CRU', if_true: files('rk3588_cru.c')) system_ss.add(when: 'CONFIG_RK3588_SCMI', if_true: files('rk3588_scmi.c')) +system_ss.add(when: 'CONFIG_SPACEMIT_K3_CTRL', if_true: files('spacemit-k3.c')) system_ss.add(when: 'CONFIG_NXP_S32_MC_ME', if_true: files('nxp_s32_mc_me.c')) specific_ss.add(when: 'CONFIG_MIPS_CPS', if_true: files('mips_cmgcr.c', 'mips_cpc.c')) diff --git a/hw/misc/spacemit-k3.c b/hw/misc/spacemit-k3.c new file mode 100644 index 0000000000000..e160f14627bcd --- /dev/null +++ b/hw/misc/spacemit-k3.c @@ -0,0 +1,206 @@ +/* + * SpacemiT K3 clock and boot controls + * + * Copyright (c) 2026 Process Mission + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qemu/bitops.h" +#include "qemu/log.h" +#include "qemu/module.h" +#include "hw/misc/spacemit-k3.h" +#include "migration/vmstate.h" + +#define K3_APMU_SDH0_CTRL 0x54 +#define K3_APMU_SDH0_AXI_RESET_N BIT(0) +#define K3_APMU_SDH0_RESET_N BIT(1) +#define K3_APMU_SDH0_AXI_CLK_EN BIT(3) +#define K3_APMU_SDH0_CLK_EN BIT(4) +#define K3_APMU_SDH0_CLK_MUX_MASK (0x7U << 5) +#define K3_APMU_SDH0_CLK_DIV_MASK (0x7U << 8) +#define K3_APMU_SDH0_CLK_DIV_DEFAULT (0x1U << 8) +#define K3_APMU_SDH0_CLK_FC BIT(11) +#define K3_APMU_SDH0_CTRL_MASK (K3_APMU_SDH0_AXI_RESET_N | \ + K3_APMU_SDH0_RESET_N | \ + K3_APMU_SDH0_AXI_CLK_EN | \ + K3_APMU_SDH0_CLK_EN | \ + K3_APMU_SDH0_CLK_MUX_MASK | \ + K3_APMU_SDH0_CLK_DIV_MASK | \ + K3_APMU_SDH0_CLK_FC) +#define K3_APMU_SDH0_CTRL_RESET (K3_APMU_SDH0_AXI_RESET_N | \ + K3_APMU_SDH0_AXI_CLK_EN | \ + K3_APMU_SDH0_CLK_EN | \ + K3_APMU_SDH0_CLK_DIV_DEFAULT) + +#define K3_CIU_BOOT_FLAG 0x110 +#define K3_CIU_BOOT_FROM_SD 0xb10 + +static uint64_t spacemit_k3_apmu_read(void *opaque, hwaddr addr, + unsigned int size) +{ + SpacemitK3APMUState *s = SPACEMIT_K3_APMU(opaque); + + switch (addr) { + case K3_APMU_SDH0_CTRL: + return s->sdh0_ctrl; + default: + qemu_log_mask(LOG_UNIMP, + "%s: unimplemented read at offset 0x%" HWADDR_PRIx + "\n", __func__, addr); + return 0; + } +} + +static void spacemit_k3_apmu_write(void *opaque, hwaddr addr, uint64_t value, + unsigned int size) +{ + SpacemitK3APMUState *s = SPACEMIT_K3_APMU(opaque); + + switch (addr) { + case K3_APMU_SDH0_CTRL: + s->sdh0_ctrl = value & K3_APMU_SDH0_CTRL_MASK & + ~K3_APMU_SDH0_CLK_FC; + break; + default: + qemu_log_mask(LOG_UNIMP, + "%s: unimplemented write at offset 0x%" HWADDR_PRIx + "\n", __func__, addr); + break; + } +} + +static const MemoryRegionOps spacemit_k3_apmu_ops = { + .read = spacemit_k3_apmu_read, + .write = spacemit_k3_apmu_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .impl = { + .min_access_size = 4, + .max_access_size = 4, + }, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + }, +}; + +static void spacemit_k3_apmu_reset(DeviceState *dev) +{ + SpacemitK3APMUState *s = SPACEMIT_K3_APMU(dev); + + s->sdh0_ctrl = K3_APMU_SDH0_CTRL_RESET; +} + +static const VMStateDescription vmstate_spacemit_k3_apmu = { + .name = TYPE_SPACEMIT_K3_APMU, + .version_id = 1, + .fields = (const VMStateField[]) { + VMSTATE_UINT32(sdh0_ctrl, SpacemitK3APMUState), + VMSTATE_END_OF_LIST(), + }, +}; + +static void spacemit_k3_apmu_realize(DeviceState *dev, Error **errp) +{ + SpacemitK3APMUState *s = SPACEMIT_K3_APMU(dev); + + memory_region_init_io(&s->mmio, OBJECT(dev), &spacemit_k3_apmu_ops, s, + TYPE_SPACEMIT_K3_APMU, + SPACEMIT_K3_APMU_MMIO_SIZE); + sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->mmio); +} + +static void spacemit_k3_apmu_class_init(ObjectClass *oc, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(oc); + + dc->realize = spacemit_k3_apmu_realize; + device_class_set_legacy_reset(dc, spacemit_k3_apmu_reset); + dc->vmsd = &vmstate_spacemit_k3_apmu; + dc->desc = "SpacemiT K3 application power management unit"; +} + +static const TypeInfo spacemit_k3_apmu_type_info = { + .name = TYPE_SPACEMIT_K3_APMU, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(SpacemitK3APMUState), + .class_init = spacemit_k3_apmu_class_init, +}; + +static uint64_t spacemit_k3_ciu_read(void *opaque, hwaddr addr, + unsigned int size) +{ + switch (addr) { + case K3_CIU_BOOT_FLAG: + return K3_CIU_BOOT_FROM_SD; + default: + qemu_log_mask(LOG_UNIMP, + "%s: unimplemented read at offset 0x%" HWADDR_PRIx + "\n", __func__, addr); + return 0; + } +} + +static void spacemit_k3_ciu_write(void *opaque, hwaddr addr, uint64_t value, + unsigned int size) +{ + switch (addr) { + case K3_CIU_BOOT_FLAG: + qemu_log_mask(LOG_GUEST_ERROR, + "%s: write to read-only boot flag\n", __func__); + break; + default: + qemu_log_mask(LOG_UNIMP, + "%s: unimplemented write at offset 0x%" HWADDR_PRIx + "\n", __func__, addr); + break; + } +} + +static const MemoryRegionOps spacemit_k3_ciu_ops = { + .read = spacemit_k3_ciu_read, + .write = spacemit_k3_ciu_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .impl = { + .min_access_size = 4, + .max_access_size = 4, + }, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + }, +}; + +static void spacemit_k3_ciu_realize(DeviceState *dev, Error **errp) +{ + SpacemitK3CIUState *s = SPACEMIT_K3_CIU(dev); + + memory_region_init_io(&s->mmio, OBJECT(dev), &spacemit_k3_ciu_ops, s, + TYPE_SPACEMIT_K3_CIU, + SPACEMIT_K3_CIU_MMIO_SIZE); + sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->mmio); +} + +static void spacemit_k3_ciu_class_init(ObjectClass *oc, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(oc); + + dc->realize = spacemit_k3_ciu_realize; + dc->desc = "SpacemiT K3 chip interface unit"; +} + +static const TypeInfo spacemit_k3_ciu_type_info = { + .name = TYPE_SPACEMIT_K3_CIU, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(SpacemitK3CIUState), + .class_init = spacemit_k3_ciu_class_init, +}; + +static void spacemit_k3_control_register_types(void) +{ + type_register_static(&spacemit_k3_apmu_type_info); + type_register_static(&spacemit_k3_ciu_type_info); +} + +type_init(spacemit_k3_control_register_types) diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig index 8365ce39ee9d4..f6bfb68ff8f8f 100644 --- a/hw/riscv/Kconfig +++ b/hw/riscv/Kconfig @@ -95,6 +95,7 @@ config SPACEMIT_K3 select RISCV_APLIC select RISCV_IMSIC select SERIAL_MM + select SPACEMIT_K3_CTRL config SHAKTI_C bool diff --git a/include/hw/misc/spacemit-k3.h b/include/hw/misc/spacemit-k3.h new file mode 100644 index 0000000000000..5b7ff65d4a487 --- /dev/null +++ b/include/hw/misc/spacemit-k3.h @@ -0,0 +1,37 @@ +/* + * SpacemiT K3 clock and boot controls + * + * Copyright (c) 2026 Process Mission + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef HW_MISC_SPACEMIT_K3_H +#define HW_MISC_SPACEMIT_K3_H + +#include "hw/core/sysbus.h" +#include "qom/object.h" + +#define TYPE_SPACEMIT_K3_APMU "spacemit.k3.apmu" +OBJECT_DECLARE_SIMPLE_TYPE(SpacemitK3APMUState, SPACEMIT_K3_APMU) + +#define TYPE_SPACEMIT_K3_CIU "spacemit.k3.ciu" +OBJECT_DECLARE_SIMPLE_TYPE(SpacemitK3CIUState, SPACEMIT_K3_CIU) + +#define SPACEMIT_K3_APMU_MMIO_SIZE 0x400 +#define SPACEMIT_K3_CIU_MMIO_SIZE 0x400 + +struct SpacemitK3APMUState { + SysBusDevice parent_obj; + + MemoryRegion mmio; + uint32_t sdh0_ctrl; +}; + +struct SpacemitK3CIUState { + SysBusDevice parent_obj; + + MemoryRegion mmio; +}; + +#endif /* HW_MISC_SPACEMIT_K3_H */ From c324741b82a8402e3e88b3ec29038907ddbd4932 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Tue, 14 Jul 2026 15:30:08 +0800 Subject: [PATCH 07/13] hw/sd: add SpacemiT K3 SDHCI controller K3 SDHCI0 follows the standard version 3 register layout but adds a small vendor register bank for card-mode and transmit-clock controls. The generic SDHCI device cannot expose those registers or the K3 capability set directly. Add a wrapper which embeds the generic controller, advertises the 3.3 V SD High Speed and ADMA2 features used by K3 U-Boot, and implements only the vendor fields touched on that boot path. Pass the SD bus and interrupt through so the board can attach storage and route IRQ 99 separately. Signed-off-by: Chao Liu --- MAINTAINERS | 2 + hw/riscv/Kconfig | 1 + hw/sd/Kconfig | 4 + hw/sd/meson.build | 1 + hw/sd/spacemit-k3-sdhci.c | 164 ++++++++++++++++++++++++++++++ include/hw/sd/spacemit-k3-sdhci.h | 33 ++++++ 6 files changed, 205 insertions(+) create mode 100644 hw/sd/spacemit-k3-sdhci.c create mode 100644 include/hw/sd/spacemit-k3-sdhci.h diff --git a/MAINTAINERS b/MAINTAINERS index 91f5beba97ef0..ec5ddc2773639 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1820,8 +1820,10 @@ L: qemu-riscv@nongnu.org S: Maintained F: hw/misc/spacemit-k3.c F: hw/riscv/spacemit-k3.c +F: hw/sd/spacemit-k3-sdhci.c F: include/hw/misc/spacemit-k3.h F: include/hw/riscv/spacemit-k3.h +F: include/hw/sd/spacemit-k3-sdhci.h F: tests/functional/riscv64/test_spacemit_k3.py F: tests/qtest/spacemit-k3-test.c diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig index f6bfb68ff8f8f..68dd5bbac97f0 100644 --- a/hw/riscv/Kconfig +++ b/hw/riscv/Kconfig @@ -96,6 +96,7 @@ config SPACEMIT_K3 select RISCV_IMSIC select SERIAL_MM select SPACEMIT_K3_CTRL + select SPACEMIT_K3_SDHCI config SHAKTI_C bool diff --git a/hw/sd/Kconfig b/hw/sd/Kconfig index ad081f7226d7c..4d98d664895fa 100644 --- a/hw/sd/Kconfig +++ b/hw/sd/Kconfig @@ -39,3 +39,7 @@ config CADENCE_SDHCI config K230_SDHCI bool select SDHCI + +config SPACEMIT_K3_SDHCI + bool + select SDHCI diff --git a/hw/sd/meson.build b/hw/sd/meson.build index 37fb5a89a891e..b637c06c87eef 100644 --- a/hw/sd/meson.build +++ b/hw/sd/meson.build @@ -14,3 +14,4 @@ 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')) +system_ss.add(when: 'CONFIG_SPACEMIT_K3_SDHCI', if_true: files('spacemit-k3-sdhci.c')) diff --git a/hw/sd/spacemit-k3-sdhci.c b/hw/sd/spacemit-k3-sdhci.c new file mode 100644 index 0000000000000..07df5743d699f --- /dev/null +++ b/hw/sd/spacemit-k3-sdhci.c @@ -0,0 +1,164 @@ +/* + * SpacemiT K3 SDHCI controller + * + * Copyright (c) 2026 Process Mission + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qemu/bitops.h" +#include "qemu/log.h" +#include "qemu/module.h" +#include "hw/core/qdev-properties.h" +#include "hw/sd/spacemit-k3-sdhci.h" +#include "migration/vmstate.h" + +#define K3_SDHCI_STD_SIZE 0x100 +#define K3_SDHCI_VENDOR_SIZE 0x100 + +/* Offsets below are relative to the vendor bank at controller offset 0x100. */ +#define K3_SDHCI_MMC_CTRL 0x14 +#define K3_SDHCI_TX_CFG 0x1c +#define K3_SDHCI_MMC_CTRL_MASK (BIT(8) | BIT(9) | BIT(10) | BIT(12)) +#define K3_SDHCI_TX_CFG_MASK (BIT(30) | BIT(31)) + +static uint64_t spacemit_k3_sdhci_vendor_read(void *opaque, hwaddr addr, + unsigned int size) +{ + SpacemitK3SDHCIState *s = SPACEMIT_K3_SDHCI(opaque); + + switch (addr) { + case K3_SDHCI_MMC_CTRL: + return s->mmc_ctrl; + case K3_SDHCI_TX_CFG: + return s->tx_cfg; + default: + qemu_log_mask(LOG_UNIMP, + "%s: unimplemented read at offset 0x%" HWADDR_PRIx + "\n", __func__, K3_SDHCI_STD_SIZE + addr); + return 0; + } +} + +static void spacemit_k3_sdhci_vendor_write(void *opaque, hwaddr addr, + uint64_t value, + unsigned int size) +{ + SpacemitK3SDHCIState *s = SPACEMIT_K3_SDHCI(opaque); + + switch (addr) { + case K3_SDHCI_MMC_CTRL: + s->mmc_ctrl = value & K3_SDHCI_MMC_CTRL_MASK; + break; + case K3_SDHCI_TX_CFG: + s->tx_cfg = value & K3_SDHCI_TX_CFG_MASK; + break; + default: + qemu_log_mask(LOG_UNIMP, + "%s: unimplemented write at offset 0x%" HWADDR_PRIx + "\n", __func__, K3_SDHCI_STD_SIZE + addr); + break; + } +} + +static const MemoryRegionOps spacemit_k3_sdhci_vendor_ops = { + .read = spacemit_k3_sdhci_vendor_read, + .write = spacemit_k3_sdhci_vendor_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .impl = { + .min_access_size = 4, + .max_access_size = 4, + }, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + }, +}; + +static void spacemit_k3_sdhci_instance_init(Object *obj) +{ + SpacemitK3SDHCIState *s = SPACEMIT_K3_SDHCI(obj); + + object_initialize_child(obj, "generic-sdhci", &s->sdhci, + TYPE_SYSBUS_SDHCI); + qdev_prop_set_uint8(DEVICE(&s->sdhci), "sd-spec-version", 3); + /* + * The core uses this property to retain v3 Host Control 2 writes. The + * capability register still advertises neither 1.8 V nor UHS modes. + */ + qdev_prop_set_uint8(DEVICE(&s->sdhci), "uhs", UHS_I); + qdev_prop_set_uint64(DEVICE(&s->sdhci), "capareg", + SPACEMIT_K3_SDHCI_CAPAREG); +} + +static void spacemit_k3_sdhci_reset(DeviceState *dev) +{ + SpacemitK3SDHCIState *s = SPACEMIT_K3_SDHCI(dev); + + s->mmc_ctrl = 0; + s->tx_cfg = 0; + device_cold_reset(DEVICE(&s->sdhci)); +} + +static void spacemit_k3_sdhci_realize(DeviceState *dev, Error **errp) +{ + SpacemitK3SDHCIState *s = SPACEMIT_K3_SDHCI(dev); + SysBusDevice *sbd = SYS_BUS_DEVICE(dev); + SysBusDevice *sdhci_sbd = SYS_BUS_DEVICE(&s->sdhci); + + memory_region_init(&s->container, OBJECT(s), + "spacemit.k3.sdhci-container", + SPACEMIT_K3_SDHCI_MMIO_SIZE); + sysbus_init_mmio(sbd, &s->container); + + memory_region_init_io(&s->vendor_iomem, OBJECT(s), + &spacemit_k3_sdhci_vendor_ops, s, + "spacemit.k3.sdhci-vendor", K3_SDHCI_VENDOR_SIZE); + memory_region_add_subregion(&s->container, K3_SDHCI_STD_SIZE, + &s->vendor_iomem); + + if (!sysbus_realize(sdhci_sbd, errp)) { + return; + } + memory_region_add_subregion(&s->container, 0, + sysbus_mmio_get_region(sdhci_sbd, 0)); + + sysbus_pass_irq(sbd, sdhci_sbd); + s->sd_bus = qdev_get_child_bus(DEVICE(sdhci_sbd), "sd-bus"); +} + +static const VMStateDescription vmstate_spacemit_k3_sdhci = { + .name = TYPE_SPACEMIT_K3_SDHCI, + .version_id = 1, + .fields = (const VMStateField[]) { + VMSTATE_UINT32(mmc_ctrl, SpacemitK3SDHCIState), + VMSTATE_UINT32(tx_cfg, SpacemitK3SDHCIState), + VMSTATE_END_OF_LIST(), + }, +}; + +static void spacemit_k3_sdhci_class_init(ObjectClass *oc, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(oc); + + dc->realize = spacemit_k3_sdhci_realize; + device_class_set_legacy_reset(dc, spacemit_k3_sdhci_reset); + dc->vmsd = &vmstate_spacemit_k3_sdhci; + dc->desc = "SpacemiT K3 SDHCI controller"; +} + +static const TypeInfo spacemit_k3_sdhci_type_info = { + .name = TYPE_SPACEMIT_K3_SDHCI, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(SpacemitK3SDHCIState), + .instance_init = spacemit_k3_sdhci_instance_init, + .class_init = spacemit_k3_sdhci_class_init, +}; + +static void spacemit_k3_sdhci_register_types(void) +{ + type_register_static(&spacemit_k3_sdhci_type_info); +} + +type_init(spacemit_k3_sdhci_register_types) diff --git a/include/hw/sd/spacemit-k3-sdhci.h b/include/hw/sd/spacemit-k3-sdhci.h new file mode 100644 index 0000000000000..d54c8e6d84bbf --- /dev/null +++ b/include/hw/sd/spacemit-k3-sdhci.h @@ -0,0 +1,33 @@ +/* + * SpacemiT K3 SDHCI controller + * + * Copyright (c) 2026 Process Mission + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef HW_SD_SPACEMIT_K3_SDHCI_H +#define HW_SD_SPACEMIT_K3_SDHCI_H + +#include "hw/core/sysbus.h" +#include "hw/sd/sdhci.h" +#include "qom/object.h" + +#define TYPE_SPACEMIT_K3_SDHCI "spacemit.k3.sdhci" +OBJECT_DECLARE_SIMPLE_TYPE(SpacemitK3SDHCIState, SPACEMIT_K3_SDHCI) + +#define SPACEMIT_K3_SDHCI_MMIO_SIZE 0x200 +#define SPACEMIT_K3_SDHCI_CAPAREG UINT64_C(0x112834b4) + +struct SpacemitK3SDHCIState { + SysBusDevice parent_obj; + + SDHCIState sdhci; + MemoryRegion container; + MemoryRegion vendor_iomem; + uint32_t mmc_ctrl; + uint32_t tx_cfg; + BusState *sd_bus; +}; + +#endif /* HW_SD_SPACEMIT_K3_SDHCI_H */ From 27f4571c006b3ff4bcf435585067399a191d7ee4 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Tue, 14 Jul 2026 15:37:02 +0800 Subject: [PATCH 08/13] hw/riscv: add SD card support to SpacemiT K3 K3 U-Boot proper expects writable on-chip SRAM, a small handoff scratch window, the SDHCI0 clock and boot controls, and an SD card on the first host. The machine currently exposes only the reset-vector page and standard RISC-V platform devices, so firmware cannot reach storage. Expand the SRAM mapping, add the reset-cleared scratch window, map the K3 control and SDHCI devices, route SDHCI0 interrupt 99 to the APLIC, and attach legacy SD unit zero. Keep BootROM, SPL, and DDR training outside the model; generic OpenSBI continues to load either Linux directly or U-Boot proper. Signed-off-by: Chao Liu --- hw/riscv/spacemit-k3.c | 129 ++++++++++++++++++++++++++------- include/hw/riscv/spacemit-k3.h | 15 +++- 2 files changed, 114 insertions(+), 30 deletions(-) diff --git a/hw/riscv/spacemit-k3.c b/hw/riscv/spacemit-k3.c index 5535225c39f2b..aeaff041d0f33 100644 --- a/hw/riscv/spacemit-k3.c +++ b/hw/riscv/spacemit-k3.c @@ -2,9 +2,10 @@ /* * SpacemiT K3 Pico-ITX machine * - * This model implements the standard RISC-V platform subset needed to boot - * the K3 SDK Linux kernel with generic OpenSBI. K3 vendor firmware and the - * A100/IME harts are intentionally outside this machine's current contract. + * This model implements the RISC-V platform and K3 boot-path subset needed + * to boot the SDK Linux kernel directly or through U-Boot with generic + * OpenSBI. BootROM, SPL, DDR training, and the A100/IME harts are outside + * this machine's current contract. */ #include "qemu/osdep.h" @@ -19,9 +20,14 @@ #include "hw/riscv/boot.h" #include "hw/riscv/machines-qom.h" #include "hw/riscv/spacemit-k3.h" +#include "hw/sd/sd.h" #include "system/address-spaces.h" +#include "system/block-backend.h" +#include "system/blockdev.h" #include "system/device_tree.h" +#include "system/memory.h" #include "system/qtest.h" +#include "system/reset.h" #include "system/system.h" #include "target/riscv/cpu.h" #include "target/riscv/cpu-qom.h" @@ -30,15 +36,19 @@ #include const MemMapEntry spacemit_k3_memmap[] = { - [K3_DEV_RESET] = { 0xc0800000, 0x1000 }, - [K3_DEV_UART0] = { 0xd4017000, 0x100 }, - [K3_DEV_S_IMSIC] = { 0xe0400000, 0x400000 }, - [K3_DEV_S_APLIC] = { 0xe0804000, 0x4000 }, - [K3_DEV_M_IMSIC] = { 0xf1000000, 0x10000 }, - [K3_DEV_M_APLIC] = { 0xf1800000, 0x4000 }, - [K3_DEV_M_CLINT] = { 0xf1810000, 0x10000 }, - [K3_DEV_FIRMWARE] = { 0x100000000, 32 * MiB }, - [K3_DEV_DRAM] = { 0x102000000, 0 }, + [K3_DEV_SRAM] = { 0xc0800000, 0x80000 }, + [K3_DEV_DDR_TRAINING] = { 0xc08d0000, 0x100 }, + [K3_DEV_UART0] = { 0xd4017000, 0x100 }, + [K3_DEV_SDHCI0] = { 0xd4280000, 0x200 }, + [K3_DEV_APMU] = { 0xd4282800, 0x400 }, + [K3_DEV_CIU] = { 0xd4282c00, 0x400 }, + [K3_DEV_S_IMSIC] = { 0xe0400000, 0x400000 }, + [K3_DEV_S_APLIC] = { 0xe0804000, 0x4000 }, + [K3_DEV_M_IMSIC] = { 0xf1000000, 0x10000 }, + [K3_DEV_M_APLIC] = { 0xf1800000, 0x4000 }, + [K3_DEV_M_CLINT] = { 0xf1810000, 0x10000 }, + [K3_DEV_FIRMWARE] = { 0x100000000, 32 * MiB }, + [K3_DEV_DRAM] = { 0x102000000, 0 }, }; static RISCVCPU *k3_pico_itx_hart(SpacemitK3SoCState *s, unsigned int hartid) @@ -80,7 +90,7 @@ static bool k3_pico_itx_validate_cpu(SpacemitK3SoCState *s, Error **errp) } if (riscv_has_ext(&cpu->env, RVH)) { error_setg(errp, - "K3 Linux-first X100 hart %u does not support H", + "K3 X100 hart %u does not support H", hartid); return false; } @@ -159,6 +169,14 @@ static void k3_pico_itx_create_aia(SpacemitK3SoCState *s) K3_PICO_ITX_APLIC_IPRIO_BITS, true, false, s->m_aplic); } +static void spacemit_k3_soc_reset(void *opaque) +{ + SpacemitK3SoCState *s = opaque; + + memset(memory_region_get_ram_ptr(&s->ddr_training), 0, + spacemit_k3_memmap[K3_DEV_DDR_TRAINING].size); +} + static void spacemit_k3_soc_realize(DeviceState *dev, Error **errp) { SpacemitK3SoCState *s = SPACEMIT_K3_SOC(dev); @@ -166,14 +184,25 @@ static void spacemit_k3_soc_realize(DeviceState *dev, Error **errp) MemoryRegion *system_memory = get_system_memory(); unsigned int cluster; - memory_region_init_rom(&s->reset, OBJECT(dev), "spacemit.k3.reset", - spacemit_k3_memmap[K3_DEV_RESET].size, errp); + memory_region_init_ram(&s->sram, OBJECT(dev), "spacemit.k3.sram", + spacemit_k3_memmap[K3_DEV_SRAM].size, errp); if (*errp) { return; } memory_region_add_subregion(system_memory, - spacemit_k3_memmap[K3_DEV_RESET].base, - &s->reset); + spacemit_k3_memmap[K3_DEV_SRAM].base, + &s->sram); + + memory_region_init_ram(&s->ddr_training, OBJECT(dev), + "spacemit.k3.ddr-training", + spacemit_k3_memmap[K3_DEV_DDR_TRAINING].size, + errp); + if (*errp) { + return; + } + memory_region_add_subregion( + system_memory, spacemit_k3_memmap[K3_DEV_DDR_TRAINING].base, + &s->ddr_training); memory_region_init_ram(&s->firmware, OBJECT(dev), "spacemit.k3.firmware", spacemit_k3_memmap[K3_DEV_FIRMWARE].size, errp); @@ -216,6 +245,27 @@ static void spacemit_k3_soc_realize(DeviceState *dev, Error **errp) k3_pico_itx_create_aia(s); + if (!sysbus_realize(SYS_BUS_DEVICE(&s->apmu), errp)) { + return; + } + sysbus_mmio_map(SYS_BUS_DEVICE(&s->apmu), 0, + spacemit_k3_memmap[K3_DEV_APMU].base); + + if (!sysbus_realize(SYS_BUS_DEVICE(&s->ciu), errp)) { + return; + } + sysbus_mmio_map(SYS_BUS_DEVICE(&s->ciu), 0, + spacemit_k3_memmap[K3_DEV_CIU].base); + + if (!sysbus_realize(SYS_BUS_DEVICE(&s->sdhci0), errp)) { + return; + } + sysbus_mmio_map(SYS_BUS_DEVICE(&s->sdhci0), 0, + spacemit_k3_memmap[K3_DEV_SDHCI0].base); + sysbus_connect_irq(SYS_BUS_DEVICE(&s->sdhci0), 0, + qdev_get_gpio_in(s->m_aplic, + K3_PICO_ITX_SDHCI0_IRQ)); + memory_region_init(&s->uart0_mem, OBJECT(dev), "spacemit.k3.uart0", spacemit_k3_memmap[K3_DEV_UART0].size); memory_region_add_subregion(system_memory, @@ -225,6 +275,8 @@ static void spacemit_k3_soc_realize(DeviceState *dev, Error **errp) &s->uart0_mem, 0, 2, qdev_get_gpio_in(s->m_aplic, K3_PICO_ITX_UART0_IRQ), 115200, serial_hd(0), DEVICE_LITTLE_ENDIAN); + + qemu_register_reset(spacemit_k3_soc_reset, s); } static void spacemit_k3_soc_instance_init(Object *obj) @@ -240,6 +292,10 @@ static void spacemit_k3_soc_instance_init(Object *obj) TYPE_CPU_CLUSTER); object_initialize_child(OBJECT(&s->clusters[1]), "cpus", &s->cpus[1], TYPE_RISCV_HART_ARRAY); + object_initialize_child(obj, "apmu", &s->apmu, TYPE_SPACEMIT_K3_APMU); + object_initialize_child(obj, "ciu", &s->ciu, TYPE_SPACEMIT_K3_CIU); + object_initialize_child(obj, "sdhci0", &s->sdhci0, + TYPE_SPACEMIT_K3_SDHCI); for (cluster = 0; cluster < K3_PICO_ITX_NUM_CLUSTERS; cluster++) { qdev_prop_set_uint32(DEVICE(&s->clusters[cluster]), "cluster-id", @@ -249,7 +305,7 @@ static void spacemit_k3_soc_instance_init(Object *obj) qdev_prop_set_uint32(DEVICE(&s->cpus[cluster]), "hartid-base", cluster * K3_PICO_ITX_HARTS_PER_CLUSTER); qdev_prop_set_uint64(DEVICE(&s->cpus[cluster]), "resetvec", - spacemit_k3_memmap[K3_DEV_RESET].base); + spacemit_k3_memmap[K3_DEV_SRAM].base); } } @@ -328,7 +384,7 @@ static void k3_pico_itx_validate_fdt(K3PicoITXState *s) machine->fdt, node); } if (fdt_path_offset(machine->fdt, "/cpus/cpu@8") >= 0) { - error_report("k3-pico-itx Linux-first DTB must not expose A100 harts"); + error_report("k3-pico-itx DTB must not expose A100 harts"); exit(EXIT_FAILURE); } @@ -351,8 +407,8 @@ static void k3_pico_itx_validate_fdt(K3PicoITXState *s) } } -static void k3_pico_itx_check_loaded_images(MachineState *machine, - RISCVBootInfo *info) +static void k3_pico_itx_check_loaded_payloads(MachineState *machine, + RISCVBootInfo *info) { hwaddr dram_base = spacemit_k3_memmap[K3_DEV_DRAM].base; hwaddr dram_end = dram_base + machine->ram_size; @@ -361,18 +417,33 @@ static void k3_pico_itx_check_loaded_images(MachineState *machine, (info->image_low_addr < dram_base || info->image_high_addr > dram_end || info->image_high_addr < info->image_low_addr)) { - error_report("K3 Linux kernel does not fit in the Linux RAM window"); + error_report("K3 -kernel payload does not fit in the DRAM window"); exit(EXIT_FAILURE); } if (info->initrd_size && (info->initrd_start < dram_base || info->initrd_start + info->initrd_size > dram_end || info->initrd_start + info->initrd_size < info->initrd_start)) { - error_report("K3 initramfs does not fit in the Linux RAM window"); + error_report("K3 -initrd payload does not fit in the DRAM window"); exit(EXIT_FAILURE); } } +static void k3_pico_itx_attach_sd_card(K3PicoITXState *s) +{ + DriveInfo *dinfo = drive_get(IF_SD, 0, 0); + DeviceState *card; + + if (!dinfo) { + return; + } + + card = qdev_new(TYPE_SD_CARD); + qdev_prop_set_drive_err(card, "drive", blk_by_legacy_dinfo(dinfo), + &error_fatal); + qdev_realize_and_unref(card, s->soc.sdhci0.sd_bus, &error_fatal); +} + static void k3_pico_itx_machine_init(MachineState *machine) { K3PicoITXState *s = K3_PICO_ITX_MACHINE(machine); @@ -389,6 +460,7 @@ static void k3_pico_itx_machine_init(MachineState *machine) spacemit_k3_memmap[K3_DEV_DRAM].base, machine->ram); qdev_realize(DEVICE(&s->soc), NULL, &error_fatal); + k3_pico_itx_attach_sd_card(s); riscv_boot_info_init(&boot_info, &s->soc.cpus[0]); @@ -427,7 +499,7 @@ static void k3_pico_itx_machine_init(MachineState *machine) spacemit_k3_memmap[K3_DEV_DRAM].base); riscv_load_kernel(machine, &boot_info, kernel_start_addr, true, NULL); - k3_pico_itx_check_loaded_images(machine, &boot_info); + k3_pico_itx_check_loaded_payloads(machine, &boot_info); kernel_entry = boot_info.image_low_addr; } @@ -438,15 +510,15 @@ static void k3_pico_itx_machine_init(MachineState *machine) if (fdt_load_addr < spacemit_k3_memmap[K3_DEV_DRAM].base || fdt_load_addr + fdt_totalsize(machine->fdt) > spacemit_k3_memmap[K3_DEV_DRAM].base + machine->ram_size) { - error_report("K3 DTB does not fit in the Linux RAM window"); + error_report("K3 DTB does not fit in the DRAM window"); exit(EXIT_FAILURE); } riscv_load_fdt(fdt_load_addr, machine->fdt); } riscv_setup_rom_reset_vec(machine, &s->soc.cpus[0], firmware_load_addr, - spacemit_k3_memmap[K3_DEV_RESET].base, - spacemit_k3_memmap[K3_DEV_RESET].size, + spacemit_k3_memmap[K3_DEV_SRAM].base, + spacemit_k3_memmap[K3_DEV_SRAM].size, kernel_entry, fdt_load_addr); } @@ -477,7 +549,7 @@ static void k3_pico_itx_machine_class_init(ObjectClass *oc, const void *data) NULL, }; - mc->desc = "SpacemiT K3 Pico-ITX (Linux-first X100 subset)"; + mc->desc = "SpacemiT K3 Pico-ITX (X100 subset)"; mc->init = k3_pico_itx_machine_init; mc->min_cpus = K3_PICO_ITX_NUM_HARTS; mc->max_cpus = K3_PICO_ITX_NUM_HARTS; @@ -486,6 +558,7 @@ static void k3_pico_itx_machine_class_init(ObjectClass *oc, const void *data) mc->valid_cpu_types = valid_cpu_types; mc->default_ram_size = 2 * GiB; mc->default_ram_id = "spacemit.k3.ram"; + mc->auto_create_sdcard = true; mc->smp_props.clusters_supported = true; compat_props_add(mc->compat_props, k3_pico_itx_cpu_defaults, G_N_ELEMENTS(k3_pico_itx_cpu_defaults)); diff --git a/include/hw/riscv/spacemit-k3.h b/include/hw/riscv/spacemit-k3.h index f6bb1e76adb4f..5bc4ec3742ed7 100644 --- a/include/hw/riscv/spacemit-k3.h +++ b/include/hw/riscv/spacemit-k3.h @@ -10,7 +10,9 @@ #include "hw/char/serial-mm.h" #include "hw/core/boards.h" #include "hw/cpu/cluster.h" +#include "hw/misc/spacemit-k3.h" #include "hw/riscv/riscv_hart.h" +#include "hw/sd/spacemit-k3-sdhci.h" #define K3_PICO_ITX_NUM_CLUSTERS 2 #define K3_PICO_ITX_HARTS_PER_CLUSTER 4 @@ -21,10 +23,15 @@ #define K3_PICO_ITX_APLIC_IPRIO_BITS 8 #define K3_PICO_ITX_IMSIC_NUM_IDS 511 #define K3_PICO_ITX_UART0_IRQ 42 +#define K3_PICO_ITX_SDHCI0_IRQ 99 enum { - K3_DEV_RESET, + K3_DEV_SRAM, + K3_DEV_DDR_TRAINING, K3_DEV_UART0, + K3_DEV_SDHCI0, + K3_DEV_APMU, + K3_DEV_CIU, K3_DEV_S_IMSIC, K3_DEV_S_APLIC, K3_DEV_M_IMSIC, @@ -44,9 +51,13 @@ struct SpacemitK3SoCState { CPUClusterState clusters[K3_PICO_ITX_NUM_CLUSTERS]; RISCVHartArrayState cpus[K3_PICO_ITX_NUM_CLUSTERS]; - MemoryRegion reset; + MemoryRegion sram; + MemoryRegion ddr_training; MemoryRegion firmware; MemoryRegion uart0_mem; + SpacemitK3APMUState apmu; + SpacemitK3CIUState ciu; + SpacemitK3SDHCIState sdhci0; DeviceState *m_imsic[K3_PICO_ITX_NUM_HARTS]; DeviceState *s_imsic[K3_PICO_ITX_NUM_HARTS]; DeviceState *m_aplic; From 9ca36d39064cbf385d12a0b892e16c54bb7548f2 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Tue, 14 Jul 2026 15:49:26 +0800 Subject: [PATCH 09/13] tests/qtest: cover SpacemiT K3 SD boot hardware The K3 machine now exposes the storage devices required by U-Boot, but the existing tests cover only the original direct-Linux platform subset. A firmware test alone would not localize regressions in reset state, DMA, or AIA routing. Extend the machine qtest for the SRAM and boot-control registers, then attach a deterministic temporary SD image and read its first sector through a 64-bit ADMA2 descriptor above 4 GiB. Verify the data and route SDHCI0 source 99 through the delegated APLIC to an S-mode IMSIC. Signed-off-by: Chao Liu --- tests/qtest/spacemit-k3-test.c | 332 ++++++++++++++++++++++++++++++--- 1 file changed, 306 insertions(+), 26 deletions(-) diff --git a/tests/qtest/spacemit-k3-test.c b/tests/qtest/spacemit-k3-test.c index 1536868d2592c..68bfa9900d625 100644 --- a/tests/qtest/spacemit-k3-test.c +++ b/tests/qtest/spacemit-k3-test.c @@ -17,10 +17,15 @@ #define K3_HARTS_PER_CLUSTER 4 #define K3_TIMEBASE_FREQ 24000000 -#define K3_RESET_BASE UINT64_C(0xc0800000) -#define K3_RESET_SIZE UINT64_C(0x1000) +#define K3_SRAM_BASE UINT64_C(0xc0800000) +#define K3_SRAM_SIZE UINT64_C(0x80000) +#define K3_DDR_TRAINING_BASE UINT64_C(0xc08d0000) +#define K3_DDR_TRAINING_SIZE UINT64_C(0x100) #define K3_UART0_BASE UINT64_C(0xd4017000) #define K3_UART0_SIZE UINT64_C(0x100) +#define K3_SDHCI0_BASE UINT64_C(0xd4280000) +#define K3_APMU_BASE UINT64_C(0xd4282800) +#define K3_CIU_BASE UINT64_C(0xd4282c00) #define K3_S_IMSIC_BASE UINT64_C(0xe0400000) #define K3_S_IMSIC_HART_STRIDE UINT64_C(0x40000) #define K3_S_APLIC_BASE UINT64_C(0xe0804000) @@ -53,6 +58,7 @@ #define APLIC_LHXW_SHIFT 12 #define K3_UART0_IRQ 42 +#define K3_SDHCI0_IRQ 99 #define UART_RBR 0x00 #define UART_IER 0x04 #define UART_LSR 0x14 @@ -60,6 +66,70 @@ #define UART_LSR_DR 0x01 #define K3_FDT_MAX_SIZE (64 * KiB) +#define K3_APMU_SDH0_CTRL 0x54 +#define K3_APMU_SDH0_RESET 0x119 +#define K3_CIU_BOOT_FLAG 0x110 +#define K3_CIU_BOOT_FROM_SD 0xb10 + +#define SDHCI_BLKSIZE 0x04 +#define SDHCI_BLKCNT 0x06 +#define SDHCI_ARGUMENT 0x08 +#define SDHCI_TRNMOD 0x0c +#define SDHCI_CMDREG 0x0e +#define SDHCI_RSPREG0 0x10 +#define SDHCI_PRNSTS 0x24 +#define SDHCI_HOSTCTL 0x28 +#define SDHCI_PWRCON 0x29 +#define SDHCI_CLKCON 0x2c +#define SDHCI_SWRST 0x2f +#define SDHCI_NORINTSTS 0x30 +#define SDHCI_NORINTSTSEN 0x34 +#define SDHCI_NORINTSIGEN 0x38 +#define SDHCI_ERRINTSIGEN 0x3a +#define SDHCI_HOSTCTL2 0x3e +#define SDHCI_CAPAB 0x40 +#define SDHCI_ADMAERR 0x54 +#define SDHCI_ADMASYSADDR 0x58 +#define SDHCI_HCVER 0xfe +#define K3_SDHCI_MMC_CTRL 0x114 +#define K3_SDHCI_TX_CFG 0x11c + +#define K3_SDHCI_CAPAB UINT64_C(0x112834b4) +#define K3_SDHCI_MMC_CTRL_MASK 0x1700 +#define K3_SDHCI_TX_CFG_MASK 0xc0000000 + +#define SDHCI_CARD_PRESENT (1U << 16) +#define SDHCI_CTRL_ADMA2_64 0x18 +#define SDHCI_POWER_330 0x0f +#define SDHCI_CLOCK_INT_EN 0x01 +#define SDHCI_CLOCK_INT_STABLE 0x02 +#define SDHCI_CLOCK_SDCLK_EN 0x04 +#define SDHCI_RESET_ALL 0x01 + +#define SDHCI_TRNS_DMA 0x0001 +#define SDHCI_TRNS_BLK_CNT_EN 0x0002 +#define SDHCI_TRNS_READ 0x0010 + +#define SDHCI_CMD_RESP_NONE 0x00 +#define SDHCI_CMD_RESP_LONG 0x01 +#define SDHCI_CMD_RESP_SHORT 0x02 +#define SDHCI_CMD_RESP_SHORT_BUSY 0x03 +#define SDHCI_CMD_CRC 0x08 +#define SDHCI_CMD_INDEX 0x10 +#define SDHCI_CMD_DATA 0x20 + +#define SDHCI_NIS_CMDCMP 0x0001 +#define SDHCI_NIS_TRSCMP 0x0002 +#define SDHCI_NIS_ERR 0x8000 + +#define SDHCI_ADMA_VALID 0x01 +#define SDHCI_ADMA_END 0x02 +#define SDHCI_ADMA_TRAN 0x20 + +#define K3_SD_SECTOR_SIZE 512 +#define K3_SD_IMAGE_SIZE (1 * MiB) +#define K3_ADMA_DESC_ADDR UINT64_C(0x102010000) +#define K3_ADMA_BUFFER_ADDR UINT64_C(0x102020000) #define CSR_MIP 0x344 #define CSR_MENVCFG 0x30a @@ -269,6 +339,32 @@ static void k3_imsic_claim(QTestState *qts, unsigned int hartid, bool mmode, k3_csr_set(qts, hartid, topei_csr, topei); } +static void k3_route_s_aplic_irq(QTestState *qts, unsigned int irq, + unsigned int eiid) +{ + const uint64_t sourcecfg = APLIC_SOURCECFG_BASE + (irq - 1) * 4; + const uint64_t target = APLIC_TARGET_BASE + (irq - 1) * 4; + + k3_imsic_enable(qts, 0, false, eiid); + + qtest_writel(qts, K3_M_APLIC_BASE + APLIC_MMSICFGADDR, + K3_M_IMSIC_BASE >> 12); + qtest_writel(qts, K3_M_APLIC_BASE + APLIC_MMSICFGADDRH, + 3U << APLIC_LHXW_SHIFT); + qtest_writel(qts, K3_M_APLIC_BASE + APLIC_SMSICFGADDR, + K3_S_IMSIC_BASE >> 12); + qtest_writel(qts, K3_M_APLIC_BASE + APLIC_SMSICFGADDRH, + 6U << APLIC_LHXS_SHIFT); + + qtest_writel(qts, K3_M_APLIC_BASE + sourcecfg, APLIC_SOURCECFG_D); + qtest_writel(qts, K3_S_APLIC_BASE + sourcecfg, + APLIC_SOURCECFG_LEVEL_HIGH); + qtest_writel(qts, K3_S_APLIC_BASE + target, eiid); + qtest_writel(qts, K3_S_APLIC_BASE + APLIC_SETIENUM, irq); + qtest_writel(qts, K3_S_APLIC_BASE + APLIC_DOMAINCFG, + APLIC_DOMAINCFG_IE); +} + static void test_topology(void) { QTestState *qts = k3_qtest_init(); @@ -318,8 +414,26 @@ static void test_address_map(void) static const uint64_t last_pattern = UINT64_C(0xfedcba9876543210); QTestState *qts = k3_qtest_init(); - g_assert_cmphex(qtest_readl(qts, K3_RESET_BASE), ==, 0x00000297); - g_assert_cmphex(qtest_readl(qts, K3_RESET_BASE + K3_RESET_SIZE), ==, 0); + g_assert_cmphex(qtest_readl(qts, K3_SRAM_BASE), ==, 0x00000297); + qtest_writeq(qts, K3_SRAM_BASE, first_pattern); + qtest_writeq(qts, K3_SRAM_BASE + K3_SRAM_SIZE - 8, last_pattern); + g_assert_cmphex(qtest_readq(qts, K3_SRAM_BASE), ==, first_pattern); + g_assert_cmphex(qtest_readq(qts, K3_SRAM_BASE + K3_SRAM_SIZE - 8), ==, + last_pattern); + g_assert_cmphex(qtest_readl(qts, K3_SRAM_BASE + K3_SRAM_SIZE), ==, 0); + + qtest_writel(qts, K3_DDR_TRAINING_BASE, 0xa5a5a5a5); + qtest_writel(qts, K3_DDR_TRAINING_BASE + K3_DDR_TRAINING_SIZE - 4, + 0x5a5a5a5a); + g_assert_cmphex(qtest_readl(qts, K3_DDR_TRAINING_BASE), ==, 0xa5a5a5a5); + g_assert_cmphex(qtest_readl(qts, K3_DDR_TRAINING_BASE + + K3_DDR_TRAINING_SIZE - 4), ==, 0x5a5a5a5a); + + qtest_system_reset(qts); + g_assert_cmphex(qtest_readl(qts, K3_SRAM_BASE), ==, 0x00000297); + g_assert_cmphex(qtest_readl(qts, K3_DDR_TRAINING_BASE), ==, 0); + g_assert_cmphex(qtest_readl(qts, K3_DDR_TRAINING_BASE + + K3_DDR_TRAINING_SIZE - 4), ==, 0); qtest_writeq(qts, K3_FIRMWARE_BASE, first_pattern); qtest_writeq(qts, K3_FIRMWARE_BASE + K3_FIRMWARE_SIZE - 8, @@ -428,10 +542,6 @@ static bool k3_wait_for_uart_rx(QTestState *qts) static void test_uart_aplic_imsic(void) { const unsigned int eiid = K3_UART0_IRQ; - const uint64_t sourcecfg = APLIC_SOURCECFG_BASE + - (K3_UART0_IRQ - 1) * 4; - const uint64_t target = APLIC_TARGET_BASE + - (K3_UART0_IRQ - 1) * 4; const uint64_t input_word = APLIC_CLRIP_BASE + (K3_UART0_IRQ / 32) * 4; const uint32_t input_mask = 1U << (K3_UART0_IRQ % 32); @@ -439,24 +549,7 @@ static void test_uart_aplic_imsic(void) QTestState *qts = qtest_init_with_serial( "-M k3-pico-itx -bios none -display none -nodefaults", &sock_fd); - k3_imsic_enable(qts, 0, false, eiid); - - qtest_writel(qts, K3_M_APLIC_BASE + APLIC_MMSICFGADDR, - K3_M_IMSIC_BASE >> 12); - qtest_writel(qts, K3_M_APLIC_BASE + APLIC_MMSICFGADDRH, - 3U << APLIC_LHXW_SHIFT); - qtest_writel(qts, K3_M_APLIC_BASE + APLIC_SMSICFGADDR, - K3_S_IMSIC_BASE >> 12); - qtest_writel(qts, K3_M_APLIC_BASE + APLIC_SMSICFGADDRH, - 6U << APLIC_LHXS_SHIFT); - - qtest_writel(qts, K3_M_APLIC_BASE + sourcecfg, APLIC_SOURCECFG_D); - qtest_writel(qts, K3_S_APLIC_BASE + sourcecfg, - APLIC_SOURCECFG_LEVEL_HIGH); - qtest_writel(qts, K3_S_APLIC_BASE + target, eiid); - qtest_writel(qts, K3_S_APLIC_BASE + APLIC_SETIENUM, K3_UART0_IRQ); - qtest_writel(qts, K3_S_APLIC_BASE + APLIC_DOMAINCFG, - APLIC_DOMAINCFG_IE); + k3_route_s_aplic_irq(qts, K3_UART0_IRQ, eiid); qtest_writel(qts, K3_UART0_BASE + UART_IER, UART_IER_RDI); g_assert_cmpint(send(sock_fd, "K", 1, 0), ==, 1); @@ -479,6 +572,189 @@ static void test_uart_aplic_imsic(void) qtest_quit(qts); } +static uint32_t k3_sdhci_cmd(QTestState *qts, unsigned int index, + uint32_t argument, uint16_t flags) +{ + uint32_t status; + + qtest_writel(qts, K3_SDHCI0_BASE + SDHCI_ARGUMENT, argument); + qtest_writew(qts, K3_SDHCI0_BASE + SDHCI_CMDREG, + index << 8 | flags); + + status = qtest_readl(qts, K3_SDHCI0_BASE + SDHCI_NORINTSTS); + g_assert_cmphex(status & (SDHCI_NIS_ERR | 0xffff0000U), ==, 0); + g_assert_cmphex(status & SDHCI_NIS_CMDCMP, ==, SDHCI_NIS_CMDCMP); + qtest_writel(qts, K3_SDHCI0_BASE + SDHCI_NORINTSTS, status); + + return qtest_readl(qts, K3_SDHCI0_BASE + SDHCI_RSPREG0); +} + +static void k3_sdhci_init_card(QTestState *qts) +{ + uint16_t rca; + + k3_sdhci_cmd(qts, 0, 0, SDHCI_CMD_RESP_NONE); + k3_sdhci_cmd(qts, 55, 0, + SDHCI_CMD_RESP_SHORT | SDHCI_CMD_CRC | SDHCI_CMD_INDEX); + k3_sdhci_cmd(qts, 41, 0x00ff8000, SDHCI_CMD_RESP_SHORT); + k3_sdhci_cmd(qts, 2, 0, SDHCI_CMD_RESP_LONG | SDHCI_CMD_CRC); + rca = k3_sdhci_cmd(qts, 3, 0, + SDHCI_CMD_RESP_SHORT | SDHCI_CMD_CRC | + SDHCI_CMD_INDEX) >> 16; + g_assert_cmpuint(rca, !=, 0); + k3_sdhci_cmd(qts, 7, (uint32_t)rca << 16, + SDHCI_CMD_RESP_SHORT_BUSY | SDHCI_CMD_CRC | + SDHCI_CMD_INDEX); +} + +static char *k3_create_sd_image(uint8_t *sector) +{ + g_autofree uint8_t *image = g_malloc0(K3_SD_IMAGE_SIZE); + g_autoptr(GError) error = NULL; + char *path = NULL; + int fd; + + for (unsigned int i = 0; i < K3_SD_SECTOR_SIZE; i++) { + sector[i] = (i * 37 + 11) & 0xff; + } + memcpy(image, sector, K3_SD_SECTOR_SIZE); + + fd = g_file_open_tmp("spacemit-k3-sd-XXXXXX.img", &path, &error); + g_assert_no_error(error); + g_assert_cmpint(fd, >=, 0); + close(fd); + g_assert_true(g_file_set_contents(path, (const char *)image, + K3_SD_IMAGE_SIZE, &error)); + g_assert_no_error(error); + + return path; +} + +static void test_sd_boot_registers(void) +{ + QTestState *qts = k3_qtest_init(); + + g_assert_cmphex(qtest_readl(qts, K3_APMU_BASE + K3_APMU_SDH0_CTRL), ==, + K3_APMU_SDH0_RESET); + qtest_writel(qts, K3_APMU_BASE + K3_APMU_SDH0_CTRL, UINT32_MAX); + g_assert_cmphex(qtest_readl(qts, K3_APMU_BASE + K3_APMU_SDH0_CTRL), ==, + 0x7fb); + + g_assert_cmphex(qtest_readl(qts, K3_CIU_BASE + K3_CIU_BOOT_FLAG), ==, + K3_CIU_BOOT_FROM_SD); + qtest_writel(qts, K3_CIU_BASE + K3_CIU_BOOT_FLAG, 0); + g_assert_cmphex(qtest_readl(qts, K3_CIU_BASE + K3_CIU_BOOT_FLAG), ==, + K3_CIU_BOOT_FROM_SD); + + g_assert_cmphex(qtest_readq(qts, K3_SDHCI0_BASE + SDHCI_CAPAB), ==, + K3_SDHCI_CAPAB); + g_assert_cmphex(qtest_readw(qts, K3_SDHCI0_BASE + SDHCI_HCVER) & 0xff, + ==, 2); + + qtest_writew(qts, K3_SDHCI0_BASE + SDHCI_HOSTCTL2, 1); + g_assert_cmphex(qtest_readw(qts, K3_SDHCI0_BASE + SDHCI_HOSTCTL2), ==, 1); + qtest_writel(qts, K3_SDHCI0_BASE + K3_SDHCI_MMC_CTRL, UINT32_MAX); + qtest_writel(qts, K3_SDHCI0_BASE + K3_SDHCI_TX_CFG, UINT32_MAX); + g_assert_cmphex(qtest_readl(qts, K3_SDHCI0_BASE + K3_SDHCI_MMC_CTRL), ==, + K3_SDHCI_MMC_CTRL_MASK); + g_assert_cmphex(qtest_readl(qts, K3_SDHCI0_BASE + K3_SDHCI_TX_CFG), ==, + K3_SDHCI_TX_CFG_MASK); + + qtest_system_reset(qts); + g_assert_cmphex(qtest_readl(qts, K3_APMU_BASE + K3_APMU_SDH0_CTRL), ==, + K3_APMU_SDH0_RESET); + g_assert_cmphex(qtest_readw(qts, K3_SDHCI0_BASE + SDHCI_HOSTCTL2), ==, 0); + g_assert_cmphex(qtest_readl(qts, K3_SDHCI0_BASE + K3_SDHCI_MMC_CTRL), ==, + 0); + g_assert_cmphex(qtest_readl(qts, K3_SDHCI0_BASE + K3_SDHCI_TX_CFG), ==, + 0); + + qtest_quit(qts); +} + +static void test_sd_adma_aplic_imsic(void) +{ + const unsigned int eiid = 11; + const uint64_t input_word = APLIC_CLRIP_BASE + + (K3_SDHCI0_IRQ / 32) * 4; + const uint32_t input_mask = 1U << (K3_SDHCI0_IRQ % 32); + const uint16_t trnmod = SDHCI_TRNS_DMA | SDHCI_TRNS_BLK_CNT_EN | + SDHCI_TRNS_READ; + uint8_t expected[K3_SD_SECTOR_SIZE]; + uint8_t actual[K3_SD_SECTOR_SIZE]; + uint8_t poison[K3_SD_SECTOR_SIZE]; + uint8_t descriptor[12] = {}; + g_autofree char *sd_path = k3_create_sd_image(expected); + QTestState *qts = qtest_initf( + "-M k3-pico-itx -bios none -display none -nodefaults " + "-drive file=%s,if=sd,format=raw,snapshot=on", sd_path); + uint32_t status; + + g_assert_cmphex(qtest_readl(qts, K3_SDHCI0_BASE + SDHCI_PRNSTS) & + SDHCI_CARD_PRESENT, ==, SDHCI_CARD_PRESENT); + qtest_writeb(qts, K3_SDHCI0_BASE + SDHCI_SWRST, SDHCI_RESET_ALL); + qtest_writeb(qts, K3_SDHCI0_BASE + SDHCI_PWRCON, SDHCI_POWER_330); + qtest_writew(qts, K3_SDHCI0_BASE + SDHCI_CLKCON, + SDHCI_CLOCK_INT_EN | SDHCI_CLOCK_SDCLK_EN); + g_assert_cmphex(qtest_readw(qts, K3_SDHCI0_BASE + SDHCI_CLKCON), ==, + SDHCI_CLOCK_INT_EN | SDHCI_CLOCK_INT_STABLE | + SDHCI_CLOCK_SDCLK_EN); + + qtest_writel(qts, K3_SDHCI0_BASE + SDHCI_NORINTSTSEN, 0xffff0003U); + k3_sdhci_init_card(qts); + k3_route_s_aplic_irq(qts, K3_SDHCI0_IRQ, eiid); + qtest_writew(qts, K3_SDHCI0_BASE + SDHCI_NORINTSIGEN, + SDHCI_NIS_TRSCMP | SDHCI_NIS_ERR); + qtest_writew(qts, K3_SDHCI0_BASE + SDHCI_ERRINTSIGEN, UINT16_MAX); + + descriptor[0] = SDHCI_ADMA_VALID | SDHCI_ADMA_END | SDHCI_ADMA_TRAN; + stw_le_p(descriptor + 2, K3_SD_SECTOR_SIZE); + stq_le_p(descriptor + 4, K3_ADMA_BUFFER_ADDR); + qtest_memwrite(qts, K3_ADMA_DESC_ADDR, descriptor, sizeof(descriptor)); + memset(poison, 0xa5, sizeof(poison)); + qtest_memwrite(qts, K3_ADMA_BUFFER_ADDR, poison, sizeof(poison)); + + qtest_writel(qts, K3_SDHCI0_BASE + SDHCI_ADMASYSADDR, + (uint32_t)K3_ADMA_DESC_ADDR); + qtest_writel(qts, K3_SDHCI0_BASE + SDHCI_ADMASYSADDR + 4, + (uint32_t)(K3_ADMA_DESC_ADDR >> 32)); + qtest_writeb(qts, K3_SDHCI0_BASE + SDHCI_HOSTCTL, + SDHCI_CTRL_ADMA2_64); + qtest_writew(qts, K3_SDHCI0_BASE + SDHCI_BLKSIZE, K3_SD_SECTOR_SIZE); + qtest_writew(qts, K3_SDHCI0_BASE + SDHCI_BLKCNT, 1); + qtest_writel(qts, K3_SDHCI0_BASE + SDHCI_ARGUMENT, 0); + qtest_writew(qts, K3_SDHCI0_BASE + SDHCI_TRNMOD, trnmod); + g_assert_cmphex(qtest_readw(qts, K3_SDHCI0_BASE + SDHCI_TRNMOD), ==, + trnmod); + qtest_writew(qts, K3_SDHCI0_BASE + SDHCI_CMDREG, + 17 << 8 | SDHCI_CMD_RESP_SHORT | SDHCI_CMD_CRC | + SDHCI_CMD_INDEX | SDHCI_CMD_DATA); + + status = qtest_readl(qts, K3_SDHCI0_BASE + SDHCI_NORINTSTS); + g_assert_cmphex(status & (SDHCI_NIS_CMDCMP | SDHCI_NIS_TRSCMP), ==, + SDHCI_NIS_CMDCMP | SDHCI_NIS_TRSCMP); + g_assert_cmphex(status & (SDHCI_NIS_ERR | 0xffff0000U), ==, 0); + g_assert_cmphex(qtest_readb(qts, K3_SDHCI0_BASE + SDHCI_ADMAERR), ==, 0); + qtest_memread(qts, K3_ADMA_BUFFER_ADDR, actual, sizeof(actual)); + g_assert_cmpmem(actual, sizeof(actual), expected, sizeof(expected)); + + g_assert_cmphex(qtest_readl(qts, K3_S_APLIC_BASE + input_word) & + input_mask, ==, input_mask); + g_assert_cmphex(k3_imsic_indirect_read(qts, 0, false, + ISELECT_IMSIC_EIP0), ==, + UINT64_C(1) << eiid); + g_assert_cmphex(k3_csr_get(qts, 0, CSR_MIP) & MIP_SEIP, ==, MIP_SEIP); + + qtest_writel(qts, K3_SDHCI0_BASE + SDHCI_NORINTSTS, status); + g_assert_cmphex(qtest_readl(qts, K3_S_APLIC_BASE + input_word) & + input_mask, ==, 0); + k3_imsic_claim(qts, 0, false, eiid); + g_assert_cmphex(k3_csr_get(qts, 0, CSR_MIP) & MIP_SEIP, ==, 0); + + qtest_quit(qts); + g_assert_cmpint(g_unlink(sd_path), ==, 0); +} + int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); @@ -496,6 +772,10 @@ int main(int argc, char **argv) qtest_add_func("spacemit-k3/imsic-routing", test_imsic_hart_routing); qtest_add_func("spacemit-k3/uart-aplic-imsic", test_uart_aplic_imsic); + qtest_add_func("spacemit-k3/sd-boot-registers", + test_sd_boot_registers); + qtest_add_func("spacemit-k3/sd-adma-aplic-imsic", + test_sd_adma_aplic_imsic); } return g_test_run(); From ca3fde0b5de167e79ee49d3928dd212f2ca49113 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 09:04:01 +0800 Subject: [PATCH 10/13] tests/functional/riscv64: boot SpacemiT K3 Linux from U-Boot Signed-off-by: Chao Liu --- tests/functional/riscv64/test_spacemit_k3.py | 36 ++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/tests/functional/riscv64/test_spacemit_k3.py b/tests/functional/riscv64/test_spacemit_k3.py index 60d75edf280c3..a7a41302a438f 100755 --- a/tests/functional/riscv64/test_spacemit_k3.py +++ b/tests/functional/riscv64/test_spacemit_k3.py @@ -5,7 +5,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later """ -Boot the pinned SpacemiT K3 SDK Linux image on k3-pico-itx. +Boot pinned SpacemiT K3 SDK Linux images on k3-pico-itx. """ from qemu_test import Asset, QemuSystemTest @@ -13,7 +13,7 @@ class SpacemitK3Test(QemuSystemTest): - """Test the Linux-first K3 Pico-ITX boot path.""" + """Test direct and U-Boot K3 Pico-ITX Linux boot paths.""" RELEASE_URL = ( 'https://github.com/zevorn/spacemit-k3-qemu-images/releases/' @@ -32,6 +32,15 @@ class SpacemitK3Test(QemuSystemTest): ASSET_DTB = Asset( RELEASE_URL + 'k3-pico-itx-qemu.dtb', '35e2fc80a64e41963d091cb23b142ed6dfc1e56f9e808061c6e241d7da4bf56e') + ASSET_UBOOT = Asset( + RELEASE_URL + 'u-boot.bin', + '02a86461f8ea30e9bae42b6c420cd9897cdc6b29b334a00b7919cb67995bd7b5') + ASSET_UBOOT_DTB = Asset( + RELEASE_URL + 'k3-pico-itx-qemu-uboot.dtb', + 'd695bac441b5a5a2814fc2ae0c5e735c11a37379e36ec59cedd16cb9f6bc3486') + ASSET_SD_IMAGE = Asset( + RELEASE_URL + 'k3-qemu-sd.raw.xz', + 'b00d9abd9c65e25346c2f76b304af0785755b2fcf49ea7faf6ec877228f32e65') def _wait_for_linux_boot(self): panic = 'Kernel panic - not syncing' @@ -81,6 +90,29 @@ def test_linux_boot(self): self.vm.launch() self._wait_for_linux_boot() + def test_uboot_sd_boot(self): + self.set_machine('k3-pico-itx') + + firmware_path = self.ASSET_FIRMWARE.fetch() + uboot_path = self.ASSET_UBOOT.fetch() + dtb_path = self.ASSET_UBOOT_DTB.fetch() + sd_path = self.uncompress(self.ASSET_SD_IMAGE) + + self.vm.add_args('-bios', firmware_path, + '-kernel', uboot_path, + '-dtb', dtb_path, + '-drive', (f'file={sd_path},if=sd,format=raw,' + 'snapshot=on'), + '-no-reboot') + self.vm.set_console() + self.vm.launch() + + panic = 'Kernel panic - not syncing' + wait_for_console_pattern(self, 'U-Boot 2022.10', panic) + wait_for_console_pattern(self, + 'K3-QEMU: Starting kernel from SD', panic) + self._wait_for_linux_boot() + if __name__ == '__main__': QemuSystemTest.main() From ca42d0c4246fe1f11c46eb49d7feacb23a635fde Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Tue, 14 Jul 2026 06:43:57 +0800 Subject: [PATCH 11/13] docs/system/riscv: document SpacemiT K3 Pico-ITX machine Describe the Linux-first X100 subset, fixed topology and memory map, supported interrupt and serial devices, direct OpenSBI boot command, and current model limitations. Add the machine and its tests to MAINTAINERS. Signed-off-by: Chao Liu --- MAINTAINERS | 1 + README.md | 1 + docs/system/riscv/spacemit-k3.rst | 95 +++++++++++++++++++++++++++++++ docs/system/target-riscv.rst | 1 + 4 files changed, 98 insertions(+) create mode 100644 docs/system/riscv/spacemit-k3.rst diff --git a/MAINTAINERS b/MAINTAINERS index ec5ddc2773639..912900615ef39 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1818,6 +1818,7 @@ SpacemiT K3 Pico-ITX M: Chao Liu L: qemu-riscv@nongnu.org S: Maintained +F: docs/system/riscv/spacemit-k3.rst F: hw/misc/spacemit-k3.c F: hw/riscv/spacemit-k3.c F: hw/sd/spacemit-k3-sdhci.c diff --git a/README.md b/README.md index a2ae8a7d2d2d2..20883ac8ea650 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ for new machine requests or bug reports. | arch | machine | direct | firmware | src | | --- | --- | --- | --- | --- | | RISC-V | [`k230-canmv`](docs/system/riscv/k230-canmv.rst) | ✅ | ✅ | PM | +| RISC-V | [`k3-pico-itx`](docs/system/riscv/spacemit-k3.rst) | ✅ | ✅ | PM | | RISC-V | [`milkv-duo`](docs/system/riscv/milkv-duo.rst) | ✅ | ✅ | UP | | RISC-V | [`riscv-server-ref`](docs/system/riscv/riscv-server-ref.rst) | ✅ | ✅ | UP | | ARM | [`phytium-pi`](docs/system/arm/phytium-pi.rst) | ✅ | ✅ | PM | diff --git a/docs/system/riscv/spacemit-k3.rst b/docs/system/riscv/spacemit-k3.rst new file mode 100644 index 0000000000000..af535bc3f966f --- /dev/null +++ b/docs/system/riscv/spacemit-k3.rst @@ -0,0 +1,95 @@ +.. SPDX-License-Identifier: GPL-2.0-or-later + +SpacemiT K3 Pico-ITX board (``k3-pico-itx``) +================================================ + +The ``k3-pico-itx`` machine models the standard RISC-V platform subset +needed to boot a SpacemiT K3 SDK Linux kernel directly or through U-Boot on +the K3 Pico-ITX board. It focuses on the eight X100 application harts and +does not expose the K3 A100 or IME harts. + +The machine has a fixed CPU topology and memory layout matching the Linux +view of the board. The default configuration must be used without overriding +the CPU type, RAM size, or SMP topology. + +Supported devices +----------------- + +The ``k3-pico-itx`` machine supports: + +* eight SpacemiT X100 harts implementing RVA23S64 in two clusters of four; +* 256-bit RISC-V vector registers, Sstc, and the Smaia/Ssaia extensions; +* 2 GiB of RAM starting at ``0x102000000``; +* a 32 MiB firmware window starting at ``0x100000000``; +* an ACLINT software interrupt and machine timer block with a 24 MHz timebase; +* machine- and supervisor-level APLIC and IMSIC interrupt controllers; +* the 8250-compatible UART0 at ``0xd4017000``, using interrupt source 42; and +* the K3 SDHCI controller at ``0xd4280000``, using interrupt source 99. + +Boot options +------------ + +Both supported Linux boot paths start generic OpenSBI directly. QEMU can +either load Linux and its initramfs, or load U-Boot proper and let U-Boot load +Linux from an SD image. An external device tree is required because the +machine validates the exact K3 topology and address map before boot. + +The following examples use artifacts built from the SpacemiT K3 Buildroot SDK +v1.0.2 and published in the ``sdk-v1.0.2-qemu2`` release of +``spacemit-k3-qemu-images``. + +Direct Linux boot +~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + $ qemu-system-riscv64 \ + -machine k3-pico-itx \ + -bios fw_dynamic.bin \ + -kernel Image \ + -initrd k3-qemu-initramfs.cpio.gz \ + -dtb k3-pico-itx-qemu.dtb \ + -append "earlycon=uart8250,mmio32,0xd4017000,115200 \ + console=ttyS0,115200 rdinit=/init" \ + -nographic -no-reboot + +The machine supplies the fixed 2 GiB RAM size and 8-hart topology, so no +``-m`` or ``-smp`` options are needed. Direct kernel boot requires +OpenSBI; ``-bios none`` is rejected when ``-kernel`` is present. +The machine uses the fixed ``spacemit-x100`` CPU model. + +U-Boot and SD boot +~~~~~~~~~~~~~~~~~~ + +Decompress ``k3-qemu-sd.raw.xz``, then start U-Boot proper as OpenSBI's next +stage: + +.. code-block:: bash + + $ xz -dk k3-qemu-sd.raw.xz + $ qemu-system-riscv64 \ + -machine k3-pico-itx \ + -bios fw_dynamic.bin \ + -kernel u-boot.bin \ + -dtb k3-pico-itx-qemu-uboot.dtb \ + -drive file=k3-qemu-sd.raw,if=sd,format=raw,snapshot=on \ + -nographic -no-reboot + +U-Boot imports its deterministic environment from the SD boot partition and +loads the Linux kernel, Linux device tree, and initramfs from that partition. +The successful path prints ``K3-QEMU: Starting kernel from SD`` before Linux +prints ``K3_LINUX_MVP_PASS``. + +Limitations +----------- + +This machine is a Linux boot subset rather than a complete K3 hardware model. +It disables the X100 H extension because the VS interrupt files and +virtualization path are not modeled. The A100 and IME harts, PCIe, +networking, multimedia accelerators, power management, and most board +peripherals are not implemented. + +The K3 BootROM, bootinfo parser, U-Boot SPL, and LPDDR training are not +modeled. QEMU provides initialized RAM and starts generic OpenSBI directly; +the firmware boot path therefore uses U-Boot proper rather than the vendor +SPL chain. diff --git a/docs/system/target-riscv.rst b/docs/system/target-riscv.rst index 9ee2011625193..546ddba16eb92 100644 --- a/docs/system/target-riscv.rst +++ b/docs/system/target-riscv.rst @@ -73,6 +73,7 @@ undocumented; you can get a complete list by running riscv/mips riscv/shakti-c riscv/sifive_u + riscv/spacemit-k3 riscv/virt riscv/xiangshan-kunminghu riscv/riscv-server-ref From 039a4b35f82a6bf5840f36f7e2db567452a46207 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 10:41:33 +0800 Subject: [PATCH 12/13] ci: disable documentation in cross builds Cross-build jobs validate compilation and installation across container toolchains. Building the same Sphinx documentation in every container adds no cross-target coverage and can fail nondeterministically when Sphinx parallel workers exit with EOFError. Disable documentation through EXTRA_CONFIGURE_OPTS for the cross-build matrix. Native jobs continue to build the documentation. Signed-off-by: Chao Liu --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e7258754c257..9a92d9d9cac81 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,7 +77,7 @@ jobs: podman run --pull newer --init --rm -it -v $(pwd):$(pwd) -w $(pwd)/tests/docker/ docker.io/pboqemu/qemu-ci:${{matrix.container}} - env QEMU_SRC=$(pwd) QEMU_WERROR="${{ env.QEMU_WERROR }}" ENABLE_RUST=0 INSTALL_DIR=/ BUILD_DIR=/tmp MAKEFLAGS="-j $(nproc)" + env QEMU_SRC=$(pwd) QEMU_WERROR="${{ env.QEMU_WERROR }}" ENABLE_RUST=0 EXTRA_CONFIGURE_OPTS=--disable-docs INSTALL_DIR=/ BUILD_DIR=/tmp MAKEFLAGS="-j $(nproc)" ./test-build build: From 733b7940705c4dacde8c535808f8cccb321eacf3 Mon Sep 17 00:00:00 2001 From: Ilya Leoshkevich Date: Wed, 15 Jul 2026 11:19:59 +0800 Subject: [PATCH 13/13] accel/tcg: make PageFlagsNodes bounds immutable page_check_range() may race with pageflags_set_clear() when the latter mutates interval-tree node bounds after a reader has found the node. This can make a valid guest range appear inaccessible and fail vma-pthread with Bad address. Keep node bounds immutable by replacing changed nodes and freeing the old ones through RCU. Guard lockless readers so they cannot use freed nodes. This is a downstream backport of upstream commit e03b7dac65d9. Signed-off-by: Chao Liu --- accel/tcg/user-exec.c | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/accel/tcg/user-exec.c b/accel/tcg/user-exec.c index d283d3cc72497..15fed1c713245 100644 --- a/accel/tcg/user-exec.c +++ b/accel/tcg/user-exec.c @@ -238,13 +238,16 @@ void page_dump(FILE *f) int page_get_flags(vaddr address) { - PageFlagsNode *p = pageflags_find(address, address); + PageFlagsNode *p; + + RCU_READ_LOCK_GUARD(); /* * See util/interval-tree.c re lockless lookups: no false positives but * there are false negatives. If we find nothing, retry with the mmap * lock acquired. */ + p = pageflags_find(address, address); if (p) { return p->flags; } @@ -300,15 +303,15 @@ static void pageflags_create_merge(vaddr start, vaddr last, int flags) if (prev) { if (next) { - prev->itree.last = next->itree.last; + pageflags_create(prev->itree.start, next->itree.last, flags); g_free_rcu(next, rcu); } else { - prev->itree.last = last; + pageflags_create(prev->itree.start, last, flags); } - interval_tree_insert(&prev->itree, &pageflags_root); + g_free_rcu(prev, rcu); } else if (next) { - next->itree.start = start; - interval_tree_insert(&next->itree, &pageflags_root); + pageflags_create(start, next->itree.last, flags); + g_free_rcu(next, rcu); } else { pageflags_create(start, last, flags); } @@ -370,8 +373,8 @@ static bool pageflags_set_clear(vaddr start, vaddr last, if (set_flags != merge_flags) { if (p_start < start) { interval_tree_remove(&p->itree, &pageflags_root); - p->itree.last = start - 1; - interval_tree_insert(&p->itree, &pageflags_root); + pageflags_create(p_start, start - 1, p_flags); + g_free_rcu(p, rcu); if (last < p_last) { if (merge_flags & PAGE_VALID) { @@ -393,11 +396,11 @@ static bool pageflags_set_clear(vaddr start, vaddr last, } if (last < p_last) { interval_tree_remove(&p->itree, &pageflags_root); - p->itree.start = last + 1; - interval_tree_insert(&p->itree, &pageflags_root); + pageflags_create(last + 1, p_last, p_flags); if (merge_flags & PAGE_VALID) { pageflags_create(start, last, merge_flags); } + g_free_rcu(p, rcu); } else { if (merge_flags & PAGE_VALID) { p->flags = merge_flags; @@ -418,8 +421,8 @@ static bool pageflags_set_clear(vaddr start, vaddr last, if (set_flags == p_flags) { if (start < p_start) { interval_tree_remove(&p->itree, &pageflags_root); - p->itree.start = start; - interval_tree_insert(&p->itree, &pageflags_root); + pageflags_create(start, p_last, p_flags); + g_free_rcu(p, rcu); } if (p_last < last) { start = p_last + 1; @@ -431,8 +434,8 @@ static bool pageflags_set_clear(vaddr start, vaddr last, /* Maybe split out head and/or tail ranges with the original flags. */ interval_tree_remove(&p->itree, &pageflags_root); if (p_start < start) { - p->itree.last = start - 1; - interval_tree_insert(&p->itree, &pageflags_root); + pageflags_create(p_start, start - 1, p_flags); + g_free_rcu(p, rcu); if (p_last < last) { goto restart; @@ -441,8 +444,8 @@ static bool pageflags_set_clear(vaddr start, vaddr last, pageflags_create(last + 1, p_last, p_flags); } } else if (last < p_last) { - p->itree.start = last + 1; - interval_tree_insert(&p->itree, &pageflags_root); + pageflags_create(last + 1, p_last, p_flags); + g_free_rcu(p, rcu); } else { g_free_rcu(p, rcu); goto restart; @@ -504,6 +507,8 @@ bool page_check_range(vaddr start, vaddr len, int flags) return false; /* wrap around */ } + RCU_READ_LOCK_GUARD(); + locked = have_mmap_lock(); while (true) { PageFlagsNode *p = pageflags_find(start, last);