From 8aa98cbf4592b12fed31201733f653405966d725 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 21:49:19 +0800 Subject: [PATCH 01/14] hw/arm: Add AX650X Pyramid machine skeleton Model the eight Cortex-A55 CPUs, high 2 GiB RAM window, GIC-400, architectural timers, PMU interrupts, PSCI and the boot UART. Generate a minimal device tree for direct Linux kernel boot. Add qtests for the CPU affinities, RAM window, GICv2 topology, virtualization interface, UART interrupt routing and reset behavior. Signed-off-by: Chao Liu --- hw/arm/Kconfig | 8 + hw/arm/ax650x.c | 404 ++++++++++++++++++++++++++++++ hw/arm/meson.build | 1 + tests/qtest/ax650x-pyramid-test.c | 155 ++++++++++++ tests/qtest/meson.build | 1 + 5 files changed, 569 insertions(+) create mode 100644 hw/arm/ax650x.c create mode 100644 tests/qtest/ax650x-pyramid-test.c diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig index 8cd2ab6db843..0c13267567b6 100644 --- a/hw/arm/Kconfig +++ b/hw/arm/Kconfig @@ -37,6 +37,14 @@ config ARM_VIRT select ACPI_CXL select ACPI_HMAT +config AX650X_PYRAMID + bool + default y + depends on TCG && AARCH64 + select ARM_GIC + select DEVICE_TREE + select SERIAL_MM + config CUBIEBOARD bool default y diff --git a/hw/arm/ax650x.c b/hw/arm/ax650x.c new file mode 100644 index 000000000000..7cb601547147 --- /dev/null +++ b/hw/arm/ax650x.c @@ -0,0 +1,404 @@ +/* + * Axera AX650X AI Pyramid machine + * + * Copyright (c) 2026 Zevorn + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qemu/error-report.h" +#include "qemu/units.h" +#include "qapi/error.h" +#include "hw/arm/boot.h" +#include "hw/arm/bsa.h" +#include "hw/arm/fdt.h" +#include "hw/arm/machines-qom.h" +#include "hw/char/serial-mm.h" +#include "hw/core/boards.h" +#include "hw/core/cpu.h" +#include "hw/core/qdev-properties.h" +#include "hw/core/sysbus.h" +#include "hw/intc/arm_gic.h" +#include "system/address-spaces.h" +#include "system/device_tree.h" +#include "system/system.h" +#include "target/arm/cpu.h" +#include "target/arm/cpu-qom.h" +#include "target/arm/gtimer.h" + +#define TYPE_AX650X_PYRAMID_MACHINE MACHINE_TYPE_NAME("ax650x-pyramid") +OBJECT_DECLARE_SIMPLE_TYPE(AX650XPyramidState, AX650X_PYRAMID_MACHINE) + +#define AX650X_NUM_CPUS 8 +#define AX650X_NUM_IRQS 256 +#define AX650X_NUM_SPIS (AX650X_NUM_IRQS - GIC_INTERNAL) + +#define AX650X_RAM_BASE 0x100000000ULL +#define AX650X_RAM_SIZE (2 * GiB) + +#define AX650X_GIC_DIST_BASE 0x04901000 +#define AX650X_GIC_DIST_SIZE 0x1000 +#define AX650X_GIC_CPU_BASE 0x04902000 +#define AX650X_GIC_CPU_SIZE 0x1000 +#define AX650X_GIC_HYP_BASE 0x04904000 +#define AX650X_GIC_HYP_SIZE 0x2000 +#define AX650X_GIC_VCPU_BASE 0x04906000 +#define AX650X_GIC_VCPU_SIZE 0x2000 + +#define AX650X_UART0_BASE 0x02016000 +#define AX650X_UART0_SIZE 0x400 +#define AX650X_UART0_IRQ 135 +#define AX650X_UART_CLOCK_HZ 200000000 +#define AX650X_UART_BAUDBASE (AX650X_UART_CLOCK_HZ / 16) + +#define AX650X_TIMER_CLOCK_HZ 24000000 +#define AX650X_PMU_IRQ_BASE 80 +#define AX650X_GIC_MAINT_PPI 9 +#define AX650X_GIC_PPI_FLAGS 0xff04 + +struct AX650XPyramidState { + MachineState parent_obj; + + ARMCPU *cpus[AX650X_NUM_CPUS]; + DeviceState *gic; + MemoryRegion gic_cpu_alias; + struct arm_boot_info bootinfo; + void *fdt; + int fdt_size; +}; + +static uint64_t ax650x_cpu_mpidr(unsigned int cpu) +{ + return (uint64_t)cpu << 8; +} + +static void ax650x_create_cpus(AX650XPyramidState *s) +{ + MachineState *machine = MACHINE(s); + MemoryRegion *sysmem = get_system_memory(); + + for (unsigned int i = 0; i < machine->smp.cpus; i++) { + Object *cpuobj = object_new(machine->cpu_type); + CPUState *cs = CPU(cpuobj); + + cs->cpu_index = i; + object_property_set_int(cpuobj, "mp-affinity", + ax650x_cpu_mpidr(i), &error_abort); + object_property_set_int(cpuobj, "cntfrq", AX650X_TIMER_CLOCK_HZ, + &error_abort); + object_property_set_bool(cpuobj, "has_el3", false, &error_abort); + object_property_set_link(cpuobj, "memory", OBJECT(sysmem), + &error_abort); + qdev_realize(DEVICE(cpuobj), NULL, &error_fatal); + s->cpus[i] = ARM_CPU(cpuobj); + object_unref(cpuobj); + } +} + +static void ax650x_create_gic(AX650XPyramidState *s) +{ + MachineState *machine = MACHINE(s); + MemoryRegion *sysmem = get_system_memory(); + SysBusDevice *gicbus; + + s->gic = qdev_new(gic_class_name()); + qdev_prop_set_uint32(s->gic, "revision", 2); + qdev_prop_set_uint32(s->gic, "num-cpu", machine->smp.cpus); + qdev_prop_set_uint32(s->gic, "num-irq", AX650X_NUM_IRQS); + qdev_prop_set_bit(s->gic, "has-security-extensions", false); + qdev_prop_set_bit(s->gic, "has-virtualization-extensions", true); + + gicbus = SYS_BUS_DEVICE(s->gic); + sysbus_realize_and_unref(gicbus, &error_fatal); + sysbus_mmio_map(gicbus, 0, AX650X_GIC_DIST_BASE); + + /* + * QEMU exposes a 0x2000-byte GICC region for GICv2, while AX650X + * decodes only the architected 0x1000-byte CPU interface window. + */ + memory_region_init_alias(&s->gic_cpu_alias, OBJECT(s), + "ax650x.gic-cpu", + sysbus_mmio_get_region(gicbus, 1), + 0, AX650X_GIC_CPU_SIZE); + memory_region_add_subregion(sysmem, AX650X_GIC_CPU_BASE, + &s->gic_cpu_alias); + sysbus_mmio_map(gicbus, 2, AX650X_GIC_HYP_BASE); + sysbus_mmio_map(gicbus, 3, AX650X_GIC_VCPU_BASE); + + for (unsigned int i = 0; i < machine->smp.cpus; i++) { + DeviceState *cpu = DEVICE(s->cpus[i]); + int ppi_base = AX650X_NUM_SPIS + i * GIC_INTERNAL; + + qdev_connect_gpio_out(cpu, GTIMER_PHYS, + qdev_get_gpio_in(s->gic, ppi_base + + ARCH_TIMER_NS_EL1_IRQ)); + qdev_connect_gpio_out(cpu, GTIMER_VIRT, + qdev_get_gpio_in(s->gic, ppi_base + + ARCH_TIMER_VIRT_IRQ)); + qdev_connect_gpio_out(cpu, GTIMER_HYP, + qdev_get_gpio_in(s->gic, ppi_base + + ARCH_TIMER_NS_EL2_IRQ)); + qdev_connect_gpio_out(cpu, GTIMER_SEC, + qdev_get_gpio_in(s->gic, ppi_base + + ARCH_TIMER_S_EL1_IRQ)); + + sysbus_connect_irq(gicbus, i, + qdev_get_gpio_in(cpu, ARM_CPU_IRQ)); + sysbus_connect_irq(gicbus, i + machine->smp.cpus, + qdev_get_gpio_in(cpu, ARM_CPU_FIQ)); + sysbus_connect_irq(gicbus, i + 2 * machine->smp.cpus, + qdev_get_gpio_in(cpu, ARM_CPU_VIRQ)); + sysbus_connect_irq(gicbus, i + 3 * machine->smp.cpus, + qdev_get_gpio_in(cpu, ARM_CPU_VFIQ)); + sysbus_connect_irq(gicbus, i + 4 * machine->smp.cpus, + qdev_get_gpio_in(s->gic, + ppi_base + AX650X_GIC_MAINT_PPI)); + + qdev_connect_gpio_out_named(cpu, "pmu-interrupt", 0, + qdev_get_gpio_in(s->gic, + AX650X_PMU_IRQ_BASE + i)); + } +} + +static void ax650x_create_fdt(AX650XPyramidState *s) +{ + MachineState *machine = MACHINE(s); + uint32_t cpu_phandles[AX650X_NUM_CPUS]; + uint32_t gic_phandle; + uint32_t uart_baud_phandle; + uint32_t uart_apb_phandle; + const char clock_names[] = "baudclk\0apb_pclk"; + const char uart_path[] = "/soc/ax_uart@2016000"; + + s->fdt = create_device_tree(&s->fdt_size); + if (!s->fdt) { + error_report("ax650x-pyramid: create_device_tree() failed"); + exit(EXIT_FAILURE); + } + + qemu_fdt_setprop_string(s->fdt, "/", "compatible", "axera,ax650x"); + qemu_fdt_setprop_string(s->fdt, "/", "model", + "M5Stack AI Pyramid (AX650X)"); + qemu_fdt_setprop_cell(s->fdt, "/", "#address-cells", 2); + qemu_fdt_setprop_cell(s->fdt, "/", "#size-cells", 2); + + qemu_fdt_add_subnode(s->fdt, "/aliases"); + qemu_fdt_add_subnode(s->fdt, "/chosen"); + qemu_fdt_setprop_string(s->fdt, "/aliases", "serial0", uart_path); + qemu_fdt_setprop_string(s->fdt, "/chosen", "stdout-path", uart_path); + + qemu_fdt_add_subnode(s->fdt, "/cpus"); + qemu_fdt_setprop_cell(s->fdt, "/cpus", "#address-cells", 1); + qemu_fdt_setprop_cell(s->fdt, "/cpus", "#size-cells", 0); + for (int i = machine->smp.cpus - 1; i >= 0; i--) { + g_autofree char *node = g_strdup_printf("/cpus/cpu@%x", + i << 8); + + qemu_fdt_add_subnode(s->fdt, node); + qemu_fdt_setprop_string(s->fdt, node, "device_type", "cpu"); + qemu_fdt_setprop_string(s->fdt, node, "compatible", + "arm,cortex-a55"); + qemu_fdt_setprop_cell(s->fdt, node, "reg", ax650x_cpu_mpidr(i)); + qemu_fdt_setprop_string(s->fdt, node, "enable-method", "psci"); + cpu_phandles[i] = qemu_fdt_alloc_phandle(s->fdt); + qemu_fdt_setprop_cell(s->fdt, node, "phandle", cpu_phandles[i]); + } + + qemu_fdt_add_subnode(s->fdt, "/psci"); + qemu_fdt_setprop_string(s->fdt, "/psci", "compatible", "arm,psci-1.0"); + qemu_fdt_setprop_string(s->fdt, "/psci", "method", "smc"); + + qemu_fdt_add_subnode(s->fdt, "/timer"); + qemu_fdt_setprop_string(s->fdt, "/timer", "compatible", + "arm,armv8-timer"); + qemu_fdt_setprop_cells(s->fdt, "/timer", "interrupts", + GIC_FDT_IRQ_TYPE_PPI, + INTID_TO_PPI(ARCH_TIMER_S_EL1_IRQ), + AX650X_GIC_PPI_FLAGS, + GIC_FDT_IRQ_TYPE_PPI, + INTID_TO_PPI(ARCH_TIMER_NS_EL1_IRQ), + AX650X_GIC_PPI_FLAGS, + GIC_FDT_IRQ_TYPE_PPI, + INTID_TO_PPI(ARCH_TIMER_VIRT_IRQ), + AX650X_GIC_PPI_FLAGS, + GIC_FDT_IRQ_TYPE_PPI, + INTID_TO_PPI(ARCH_TIMER_NS_EL2_IRQ), + AX650X_GIC_PPI_FLAGS); + qemu_fdt_setprop_cell(s->fdt, "/timer", "clock-frequency", + AX650X_TIMER_CLOCK_HZ); + + qemu_fdt_add_subnode(s->fdt, "/interrupt-controller@4900000"); + qemu_fdt_setprop_string(s->fdt, "/interrupt-controller@4900000", + "compatible", "arm,gic-400"); + qemu_fdt_setprop_cell(s->fdt, "/interrupt-controller@4900000", + "#interrupt-cells", 3); + qemu_fdt_setprop_cell(s->fdt, "/interrupt-controller@4900000", + "#address-cells", 2); + qemu_fdt_setprop_cell(s->fdt, "/interrupt-controller@4900000", + "#size-cells", 2); + qemu_fdt_setprop(s->fdt, "/interrupt-controller@4900000", + "interrupt-controller", NULL, 0); + qemu_fdt_setprop(s->fdt, "/interrupt-controller@4900000", "ranges", + NULL, 0); + qemu_fdt_setprop_sized_cells(s->fdt, "/interrupt-controller@4900000", + "reg", + 2, AX650X_GIC_DIST_BASE, + 2, AX650X_GIC_DIST_SIZE, + 2, AX650X_GIC_CPU_BASE, + 2, AX650X_GIC_CPU_SIZE, + 2, AX650X_GIC_HYP_BASE, + 2, AX650X_GIC_HYP_SIZE, + 2, AX650X_GIC_VCPU_BASE, + 2, AX650X_GIC_VCPU_SIZE); + qemu_fdt_setprop_cells(s->fdt, "/interrupt-controller@4900000", + "interrupts", GIC_FDT_IRQ_TYPE_PPI, + AX650X_GIC_MAINT_PPI, AX650X_GIC_PPI_FLAGS); + gic_phandle = qemu_fdt_alloc_phandle(s->fdt); + qemu_fdt_setprop_cell(s->fdt, "/interrupt-controller@4900000", + "phandle", gic_phandle); + qemu_fdt_setprop_cell(s->fdt, "/", "interrupt-parent", gic_phandle); + + qemu_fdt_add_subnode(s->fdt, "/pmu"); + qemu_fdt_setprop_string(s->fdt, "/pmu", "compatible", + "arm,cortex-a55-pmu"); + qemu_fdt_setprop_cells(s->fdt, "/pmu", "interrupts", + GIC_FDT_IRQ_TYPE_SPI, 80, + GIC_FDT_IRQ_FLAGS_LEVEL_HI, + GIC_FDT_IRQ_TYPE_SPI, 81, + GIC_FDT_IRQ_FLAGS_LEVEL_HI, + GIC_FDT_IRQ_TYPE_SPI, 82, + GIC_FDT_IRQ_FLAGS_LEVEL_HI, + GIC_FDT_IRQ_TYPE_SPI, 83, + GIC_FDT_IRQ_FLAGS_LEVEL_HI, + GIC_FDT_IRQ_TYPE_SPI, 84, + GIC_FDT_IRQ_FLAGS_LEVEL_HI, + GIC_FDT_IRQ_TYPE_SPI, 85, + GIC_FDT_IRQ_FLAGS_LEVEL_HI, + GIC_FDT_IRQ_TYPE_SPI, 86, + GIC_FDT_IRQ_FLAGS_LEVEL_HI, + GIC_FDT_IRQ_TYPE_SPI, 87, + GIC_FDT_IRQ_FLAGS_LEVEL_HI); + qemu_fdt_setprop_cells(s->fdt, "/pmu", "interrupt-affinity", + cpu_phandles[0], cpu_phandles[1], + cpu_phandles[2], cpu_phandles[3], + cpu_phandles[4], cpu_phandles[5], + cpu_phandles[6], cpu_phandles[7]); + + qemu_fdt_add_subnode(s->fdt, "/uart-baud-clock"); + qemu_fdt_setprop_string(s->fdt, "/uart-baud-clock", "compatible", + "fixed-clock"); + qemu_fdt_setprop_cell(s->fdt, "/uart-baud-clock", "#clock-cells", 0); + qemu_fdt_setprop_cell(s->fdt, "/uart-baud-clock", "clock-frequency", + AX650X_UART_CLOCK_HZ); + uart_baud_phandle = qemu_fdt_alloc_phandle(s->fdt); + qemu_fdt_setprop_cell(s->fdt, "/uart-baud-clock", "phandle", + uart_baud_phandle); + + qemu_fdt_add_subnode(s->fdt, "/uart-apb-clock"); + qemu_fdt_setprop_string(s->fdt, "/uart-apb-clock", "compatible", + "fixed-clock"); + qemu_fdt_setprop_cell(s->fdt, "/uart-apb-clock", "#clock-cells", 0); + qemu_fdt_setprop_cell(s->fdt, "/uart-apb-clock", "clock-frequency", + AX650X_UART_CLOCK_HZ); + uart_apb_phandle = qemu_fdt_alloc_phandle(s->fdt); + qemu_fdt_setprop_cell(s->fdt, "/uart-apb-clock", "phandle", + uart_apb_phandle); + + qemu_fdt_add_subnode(s->fdt, "/soc"); + qemu_fdt_setprop_string(s->fdt, "/soc", "compatible", "simple-bus"); + qemu_fdt_setprop_cell(s->fdt, "/soc", "#address-cells", 2); + qemu_fdt_setprop_cell(s->fdt, "/soc", "#size-cells", 2); + qemu_fdt_setprop(s->fdt, "/soc", "ranges", NULL, 0); + + qemu_fdt_add_subnode(s->fdt, uart_path); + qemu_fdt_setprop_string(s->fdt, uart_path, "compatible", + "axera,ax-apb-uart"); + qemu_fdt_setprop_sized_cells(s->fdt, uart_path, "reg", + 2, AX650X_UART0_BASE, + 2, AX650X_UART0_SIZE); + qemu_fdt_setprop_cell(s->fdt, uart_path, "reg-shift", 2); + qemu_fdt_setprop_cell(s->fdt, uart_path, "reg-io-width", 4); + qemu_fdt_setprop_cells(s->fdt, uart_path, "clocks", + uart_baud_phandle, uart_apb_phandle); + qemu_fdt_setprop(s->fdt, uart_path, "clock-names", clock_names, + sizeof(clock_names)); + qemu_fdt_setprop_cell(s->fdt, uart_path, "default_cpr_reg", 0x425f2); + qemu_fdt_setprop_cells(s->fdt, uart_path, "interrupts", + GIC_FDT_IRQ_TYPE_SPI, AX650X_UART0_IRQ, + GIC_FDT_IRQ_FLAGS_LEVEL_HI); + qemu_fdt_setprop_string(s->fdt, uart_path, "status", "okay"); +} + +static void *ax650x_get_dtb(const struct arm_boot_info *binfo, int *fdt_size) +{ + AX650XPyramidState *s = container_of(binfo, AX650XPyramidState, + bootinfo); + + *fdt_size = s->fdt_size; + return s->fdt; +} + +static void ax650x_pyramid_init(MachineState *machine) +{ + AX650XPyramidState *s = AX650X_PYRAMID_MACHINE(machine); + MemoryRegion *sysmem = get_system_memory(); + + if (machine->ram_size != AX650X_RAM_SIZE) { + error_report("ax650x-pyramid: RAM size must be exactly 2 GiB"); + exit(EXIT_FAILURE); + } + + memory_region_add_subregion(sysmem, AX650X_RAM_BASE, machine->ram); + ax650x_create_cpus(s); + ax650x_create_gic(s); + + serial_mm_init(sysmem, AX650X_UART0_BASE, 2, + qdev_get_gpio_in(s->gic, AX650X_UART0_IRQ), + AX650X_UART_BAUDBASE, serial_hd(0), DEVICE_LITTLE_ENDIAN); + + ax650x_create_fdt(s); + + s->bootinfo.ram_size = machine->ram_size; + s->bootinfo.board_id = -1; + s->bootinfo.loader_start = AX650X_RAM_BASE; + s->bootinfo.get_dtb = ax650x_get_dtb; + s->bootinfo.psci_conduit = QEMU_PSCI_CONDUIT_SMC; + arm_load_kernel(s->cpus[0], machine, &s->bootinfo); +} + +static void ax650x_pyramid_class_init(ObjectClass *oc, const void *data) +{ + MachineClass *mc = MACHINE_CLASS(oc); + static const char * const valid_cpu_types[] = { + ARM_CPU_TYPE_NAME("cortex-a55"), + NULL, + }; + + mc->desc = "M5Stack AI Pyramid (Axera AX650X)"; + mc->init = ax650x_pyramid_init; + mc->default_cpu_type = ARM_CPU_TYPE_NAME("cortex-a55"); + mc->valid_cpu_types = valid_cpu_types; + mc->default_cpus = AX650X_NUM_CPUS; + mc->min_cpus = AX650X_NUM_CPUS; + mc->max_cpus = AX650X_NUM_CPUS; + mc->default_ram_size = AX650X_RAM_SIZE; + mc->default_ram_id = "ax650x.ram"; + mc->no_cdrom = true; + mc->no_floppy = true; +} + +static const TypeInfo ax650x_pyramid_info = { + .name = TYPE_AX650X_PYRAMID_MACHINE, + .parent = TYPE_MACHINE, + .instance_size = sizeof(AX650XPyramidState), + .class_init = ax650x_pyramid_class_init, + .interfaces = aarch64_machine_interfaces, +}; + +static void ax650x_pyramid_machine_register_types(void) +{ + type_register_static(&ax650x_pyramid_info); +} + +type_init(ax650x_pyramid_machine_register_types) diff --git a/hw/arm/meson.build b/hw/arm/meson.build index 9024a304f10f..9642db354d30 100644 --- a/hw/arm/meson.build +++ b/hw/arm/meson.build @@ -1,5 +1,6 @@ arm_common_ss = ss.source_set() arm_common_ss.add(when: 'CONFIG_ARM_VIRT', if_true: files('virt.c')) +arm_common_ss.add(when: 'CONFIG_AX650X_PYRAMID', if_true: files('ax650x.c')) arm_common_ss.add(when: 'CONFIG_ACPI', if_true: files('virt-acpi-build.c')) arm_common_ss.add(when: 'CONFIG_DIGIC', if_true: files('digic_boards.c')) arm_common_ss.add(when: 'CONFIG_EMCRAFT_SF2', if_true: files('msf2-som.c')) diff --git a/tests/qtest/ax650x-pyramid-test.c b/tests/qtest/ax650x-pyramid-test.c new file mode 100644 index 000000000000..f53eb30164c0 --- /dev/null +++ b/tests/qtest/ax650x-pyramid-test.c @@ -0,0 +1,155 @@ +/* + * QTest for the M5Stack AI Pyramid (Axera AX650X) + * + * Copyright (c) 2026 Zevorn + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qemu/units.h" +#include "qobject/qdict.h" +#include "qobject/qlist.h" +#include "qobject/qnum.h" +#include "libqtest.h" + +#define AX650X_NUM_CPUS 8 + +#define AX650X_RAM_BASE 0x100000000ULL +#define AX650X_RESET_RAM_TEST_ADDR (AX650X_RAM_BASE + 256 * MiB) + +#define AX650X_GIC_DIST_BASE 0x04901000 +#define AX650X_GIC_HYP_BASE 0x04904000 +#define AX650X_GICD_CTLR 0x000 +#define AX650X_GICD_TYPER 0x004 +#define AX650X_GICD_ISPENDR 0x200 +#define AX650X_GICH_VTR 0x004 + +#define AX650X_UART0_BASE 0x02016000 +#define AX650X_UART_REGSHIFT 2 +#define AX650X_UART_IER (1 << AX650X_UART_REGSHIFT) +#define AX650X_UART_LSR (5 << AX650X_UART_REGSHIFT) +#define AX650X_UART_IER_THRI 0x02 +#define AX650X_UART_LSR_THRE 0x20 +#define AX650X_UART_LSR_TEMT 0x40 +#define AX650X_UART0_INTID (32 + 135) + +static QTestState *ax650x_pyramid_start(void) +{ + return qtest_init("-machine ax650x-pyramid -accel qtest -display none"); +} + +static uint64_t qom_get_uint(QTestState *qts, const char *path, + const char *property) +{ + QDict *response; + QNum *number; + uint64_t value; + + response = qtest_qmp(qts, + "{ 'execute': 'qom-get'," + " 'arguments': { 'path': %s, 'property': %s } }", + path, property); + g_assert(qdict_haskey(response, "return")); + number = qobject_to(QNum, qdict_get(response, "return")); + g_assert_nonnull(number); + g_assert_true(qnum_get_try_uint(number, &value)); + qobject_unref(response); + + return value; +} + +static void test_cpu_topology(void) +{ + QTestState *qts = ax650x_pyramid_start(); + QDict *response; + QList *cpus; + QListEntry *entry; + + 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), ==, AX650X_NUM_CPUS); + + QLIST_FOREACH_ENTRY(cpus, entry) { + QDict *cpu = qobject_to(QDict, qlist_entry_obj(entry)); + unsigned int index = qdict_get_int(cpu, "cpu-index"); + const char *path = qdict_get_str(cpu, "qom-path"); + + g_assert_cmpuint(index, <, AX650X_NUM_CPUS); + g_assert_cmphex(qom_get_uint(qts, path, "mp-affinity"), ==, + (uint64_t)index << 8); + } + + qobject_unref(response); + qtest_quit(qts); +} + +static void test_memory_and_gic(void) +{ + QTestState *qts = ax650x_pyramid_start(); + uint64_t pattern = 0x0123456789abcdefULL; + uint32_t typer; + uint32_t vtr; + + qtest_writeq(qts, AX650X_RAM_BASE + 0x1000, pattern); + g_assert_cmphex(qtest_readq(qts, AX650X_RAM_BASE + 0x1000), ==, + pattern); + + g_assert_cmphex(qtest_readl(qts, AX650X_GIC_DIST_BASE + AX650X_GICD_CTLR), + ==, 0); + typer = qtest_readl(qts, AX650X_GIC_DIST_BASE + AX650X_GICD_TYPER); + g_assert_cmpuint(typer & 0x1f, ==, 7); + g_assert_cmpuint((typer >> 5) & 0x7, ==, AX650X_NUM_CPUS - 1); + + vtr = qtest_readl(qts, AX650X_GIC_HYP_BASE + AX650X_GICH_VTR); + g_assert_cmpuint(vtr & 0x3f, ==, 3); + + qtest_quit(qts); +} + +static void test_uart_irq_and_reset(void) +{ + QTestState *qts = ax650x_pyramid_start(); + uint64_t ram_pattern = 0xfedcba9876543210ULL; + uint64_t pending_addr; + uint32_t pending_mask; + uint32_t lsr; + + lsr = qtest_readl(qts, AX650X_UART0_BASE + AX650X_UART_LSR); + g_assert_cmphex(lsr & (AX650X_UART_LSR_THRE | AX650X_UART_LSR_TEMT), ==, + AX650X_UART_LSR_THRE | AX650X_UART_LSR_TEMT); + g_assert_cmphex(qtest_readl(qts, AX650X_UART0_BASE + AX650X_UART_IER), + ==, 0); + + qtest_writel(qts, AX650X_UART0_BASE + AX650X_UART_IER, + AX650X_UART_IER_THRI); + pending_addr = AX650X_GIC_DIST_BASE + AX650X_GICD_ISPENDR + + (AX650X_UART0_INTID / 32) * sizeof(uint32_t); + pending_mask = 1U << (AX650X_UART0_INTID % 32); + g_assert_cmphex(qtest_readl(qts, pending_addr) & pending_mask, ==, + pending_mask); + + qtest_writeq(qts, AX650X_RESET_RAM_TEST_ADDR, ram_pattern); + qtest_system_reset(qts); + g_assert_cmphex(qtest_readl(qts, AX650X_UART0_BASE + AX650X_UART_IER), + ==, 0); + g_assert_cmphex(qtest_readl(qts, AX650X_GIC_DIST_BASE + AX650X_GICD_CTLR), + ==, 0); + g_assert_cmphex(qtest_readq(qts, AX650X_RESET_RAM_TEST_ADDR), ==, + ram_pattern); + + qtest_quit(qts); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + + qtest_add_func("ax650x-pyramid/cpu-topology", test_cpu_topology); + qtest_add_func("ax650x-pyramid/memory-and-gic", test_memory_and_gic); + qtest_add_func("ax650x-pyramid/uart-irq-and-reset", + test_uart_irq_and_reset); + + return g_test_run(); +} diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build index 48359142c3cf..8a45bc9ad9dc 100644 --- a/tests/qtest/meson.build +++ b/tests/qtest/meson.build @@ -258,6 +258,7 @@ qtests_arm = \ # TODO: once aarch64 TCG is fixed on ARM 32 bit host, make bios-tables-test unconditional qtests_aarch64 = \ (cpu != 'arm' and unpack_edk2_blobs ? ['bios-tables-test'] : []) + \ + (config_all_devices.has_key('CONFIG_AX650X_PYRAMID') ? ['ax650x-pyramid-test'] : []) + \ (config_all_accel.has_key('CONFIG_TCG') and config_all_devices.has_key('CONFIG_TPM_TIS_SYSBUS') ? \ ['tpm-tis-device-test', 'tpm-tis-device-swtpm-test'] : []) + \ (config_all_devices.has_key('CONFIG_XLNX_ZYNQMP_ARM') ? ['xlnx-can-test', 'fuzz-xlnx-dp-test'] : []) + \ From e09d5fcb67bc021c90e53d62b3707b6209d633a4 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 12:35:58 +0800 Subject: [PATCH 02/14] hw/sd: Add AX650X SDHCI controller Model the AX650X SDHCI wrapper around QEMU's generic controller. Add the vendor pointer, PHY/DLL register bank, eMMC reset control, and the embedded-card capability used by the vendor Linux driver. Signed-off-by: Chao Liu --- hw/arm/Kconfig | 1 + hw/sd/Kconfig | 4 + hw/sd/ax650x-sdhci.c | 244 +++++++++++++++++++++++++++++++++++ hw/sd/meson.build | 1 + include/hw/sd/ax650x-sdhci.h | 75 +++++++++++ 5 files changed, 325 insertions(+) create mode 100644 hw/sd/ax650x-sdhci.c create mode 100644 include/hw/sd/ax650x-sdhci.h diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig index 0c13267567b6..239ca712039e 100644 --- a/hw/arm/Kconfig +++ b/hw/arm/Kconfig @@ -42,6 +42,7 @@ config AX650X_PYRAMID default y depends on TCG && AARCH64 select ARM_GIC + select AX650X_SDHCI select DEVICE_TREE select SERIAL_MM diff --git a/hw/sd/Kconfig b/hw/sd/Kconfig index 4d98d664895f..7850afe7f9d1 100644 --- a/hw/sd/Kconfig +++ b/hw/sd/Kconfig @@ -43,3 +43,7 @@ config K230_SDHCI config SPACEMIT_K3_SDHCI bool select SDHCI + +config AX650X_SDHCI + bool + select SDHCI diff --git a/hw/sd/ax650x-sdhci.c b/hw/sd/ax650x-sdhci.c new file mode 100644 index 000000000000..c983078daf79 --- /dev/null +++ b/hw/sd/ax650x-sdhci.c @@ -0,0 +1,244 @@ +/* + * Axera AX650X SDHCI controller + * + * Copyright (c) 2026 Zevorn + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "hw/core/qdev-properties.h" +#include "hw/sd/ax650x-sdhci.h" +#include "migration/vmstate.h" +#include "sdhci-internal.h" + +#define AX650X_VENDOR_ADDR(_addr) ((_addr) - AX650X_SDHCI_VENDOR_BASE) + +#define VREG(_name, _addr) \ + { .name = (_name), .addr = AX650X_VENDOR_ADDR(_addr) } +#define VREG_RO(_name, _addr, _reset) \ + { .name = (_name), .addr = AX650X_VENDOR_ADDR(_addr), \ + .reset = (_reset), .ro = UINT8_MAX } + +static uint64_t ax650x_register_bank_read(void *opaque, hwaddr addr, + unsigned int size) +{ + uint64_t value = 0; + + for (unsigned int i = 0; i < size; i++) { + value |= register_read_memory(opaque, addr + i, 1) << (i * 8); + } + + return value; +} + +static void ax650x_register_bank_write(void *opaque, hwaddr addr, + uint64_t value, unsigned int size) +{ + for (unsigned int i = 0; i < size; i++) { + register_write_memory(opaque, addr + i, value >> (i * 8), 1); + } +} + +static const MemoryRegionOps ax650x_register_bank_ops = { + .read = ax650x_register_bank_read, + .write = ax650x_register_bank_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .impl = { + .min_access_size = 1, + .max_access_size = 4, + .unaligned = true, + }, + .valid = { + .min_access_size = 1, + .max_access_size = 4, + .unaligned = true, + }, +}; + +static void ax650x_dll_ctrl_post_write(RegisterInfo *reg, uint64_t value) +{ + AX650XSDHCIState *s = AX650X_SDHCI(reg->opaque); + unsigned int status = AX650X_VENDOR_ADDR(AX650X_SDHCI_PHY_DLL_STATUS); + + if (value & AX650X_SDHCI_DLL_EN) { + s->vendor_regs[status] = AX650X_SDHCI_DLL_LOCKED; + } else { + s->vendor_regs[status] = 0; + } +} + +static void ax650x_emmc_ctrl_post_write(RegisterInfo *reg, uint64_t value) +{ + AX650XSDHCIState *s = AX650X_SDHCI(reg->opaque); + BusChild *child; + bool reset_asserted; + + reset_asserted = (value & AX650X_SDHCI_EMMC_RST_N_OE) && + !(value & AX650X_SDHCI_EMMC_RST_N); + if (s->emmc_reset_asserted && !reset_asserted) { + child = QTAILQ_FIRST(&s->sdhci.sdbus.qbus.children); + if (child) { + device_cold_reset(child->child); + } + } + s->emmc_reset_asserted = reset_asserted; +} + +static const RegisterAccessInfo ax650x_pointer_regs_info[] = { + { .name = "P_VENDOR_SPECIFIC_AREA[7:0]", .addr = 0, + .reset = AX650X_SDHCI_VENDOR_PTR_VALUE & 0xff, .ro = UINT8_MAX }, + { .name = "P_VENDOR_SPECIFIC_AREA[15:8]", .addr = 1, + .reset = AX650X_SDHCI_VENDOR_PTR_VALUE >> 8, .ro = UINT8_MAX }, +}; + +static const RegisterAccessInfo ax650x_vendor_regs_info[] = { + { .name = "PHY_CNFG[7:0]", + .addr = AX650X_VENDOR_ADDR(AX650X_SDHCI_PHY_CNFG), + .reset = AX650X_SDHCI_PHY_PWRGOOD, + .ro = AX650X_SDHCI_PHY_PWRGOOD }, + VREG("PHY_CNFG[15:8]", AX650X_SDHCI_PHY_CNFG + 1), + VREG("PHY_CNFG[23:16]", AX650X_SDHCI_PHY_CNFG + 2), + VREG("PHY_CNFG[31:24]", AX650X_SDHCI_PHY_CNFG + 3), + VREG("PHY_CMDPAD_CNFG[7:0]", AX650X_SDHCI_PHY_CMDPAD_CNFG), + VREG("PHY_CMDPAD_CNFG[15:8]", AX650X_SDHCI_PHY_CMDPAD_CNFG + 1), + VREG("PHY_DATAPAD_CNFG[7:0]", AX650X_SDHCI_PHY_DATAPAD_CNFG), + VREG("PHY_DATAPAD_CNFG[15:8]", AX650X_SDHCI_PHY_DATAPAD_CNFG + 1), + VREG("PHY_CLKPAD_CNFG[7:0]", AX650X_SDHCI_PHY_CLKPAD_CNFG), + VREG("PHY_CLKPAD_CNFG[15:8]", AX650X_SDHCI_PHY_CLKPAD_CNFG + 1), + VREG("PHY_STBPAD_CNFG[7:0]", AX650X_SDHCI_PHY_STBPAD_CNFG), + VREG("PHY_STBPAD_CNFG[15:8]", AX650X_SDHCI_PHY_STBPAD_CNFG + 1), + VREG("PHY_RSTNPAD_CNFG[7:0]", AX650X_SDHCI_PHY_RSTNPAD_CNFG), + VREG("PHY_RSTNPAD_CNFG[15:8]", AX650X_SDHCI_PHY_RSTNPAD_CNFG + 1), + VREG("PHY_COMMDL_CNFG", AX650X_SDHCI_PHY_COMMDL_CNFG), + VREG("PHY_SDCLKDL_CNFG", AX650X_SDHCI_PHY_SDCLKDL_CNFG), + VREG("PHY_SDCLKDL_DC", AX650X_SDHCI_PHY_SDCLKDL_DC), + VREG("PHY_SMPLDL_CNFG", AX650X_SDHCI_PHY_SMPLDL_CNFG), + VREG("PHY_ATDL_CNFG", AX650X_SDHCI_PHY_ATDL_CNFG), + { .name = "PHY_DLL_CTRL", + .addr = AX650X_VENDOR_ADDR(AX650X_SDHCI_PHY_DLL_CTRL), + .rsvd = 0xf8, + .post_write = ax650x_dll_ctrl_post_write }, + VREG("PHY_DLL_CNFG1", AX650X_SDHCI_PHY_DLL_CNFG1), + VREG("PHY_DLL_CNFG2", AX650X_SDHCI_PHY_DLL_CNFG2), + VREG("PHY_DLLDL_CNFG", AX650X_SDHCI_PHY_DLLDL_CNFG), + VREG("PHY_DLL_OFFSET", AX650X_SDHCI_PHY_DLL_OFFSET), + VREG("PHY_DLLLBT_CNFG[7:0]", AX650X_SDHCI_PHY_DLLLBT_CNFG), + VREG("PHY_DLLLBT_CNFG[15:8]", AX650X_SDHCI_PHY_DLLLBT_CNFG + 1), + VREG_RO("PHY_DLL_STATUS", AX650X_SDHCI_PHY_DLL_STATUS, 0), + VREG_RO("PHY_DLLDBG_MLKDC", AX650X_SDHCI_PHY_DLLDBG_MLKDC, 98), + VREG_RO("PHY_DLLDBG_SLKDC", AX650X_SDHCI_PHY_DLLDBG_SLKDC, 24), + { .name = "EMMC_CTRL[7:0]", + .addr = AX650X_VENDOR_ADDR(AX650X_SDHCI_EMMC_CTRL), + .rsvd = 0xf2, + .post_write = ax650x_emmc_ctrl_post_write }, + { .name = "EMMC_CTRL[15:8]", + .addr = AX650X_VENDOR_ADDR(AX650X_SDHCI_EMMC_CTRL + 1), + .rsvd = 0xfe }, +}; + +static void ax650x_sdhci_reset(DeviceState *dev) +{ + AX650XSDHCIState *s = AX650X_SDHCI(dev); + + device_cold_reset(DEVICE(&s->sdhci)); + for (unsigned int i = 0; + i < s->pointer_reg_array->num_elements; i++) { + register_reset(s->pointer_reg_array->r[i]); + } + for (unsigned int i = 0; + i < s->vendor_reg_array->num_elements; i++) { + register_reset(s->vendor_reg_array->r[i]); + } + s->emmc_reset_asserted = false; +} + +static void ax650x_sdhci_realize(DeviceState *dev, Error **errp) +{ + AX650XSDHCIState *s = AX650X_SDHCI(dev); + SysBusDevice *sbd = SYS_BUS_DEVICE(dev); + SysBusDevice *sdhci_sbd = SYS_BUS_DEVICE(&s->sdhci); + + qdev_prop_set_uint8(DEVICE(&s->sdhci), "sd-spec-version", 3); + qdev_prop_set_uint8(DEVICE(&s->sdhci), "uhs", UHS_I); + qdev_prop_set_uint64(DEVICE(&s->sdhci), "capareg", + SDHC_CAPAB_REG_DEFAULT | BIT_ULL(18)); + if (!sysbus_realize(sdhci_sbd, errp)) { + return; + } + + memory_region_init(&s->container, OBJECT(s), "ax650x.sdhci-container", + AX650X_SDHCI_REG_SIZE); + sysbus_init_mmio(sbd, &s->container); + memory_region_add_subregion(&s->container, 0, + sysbus_mmio_get_region(sdhci_sbd, 0)); + memory_region_add_subregion_overlap(&s->container, + AX650X_SDHCI_VENDOR_PTR, + &s->pointer_reg_array->mem, 1); + memory_region_add_subregion(&s->container, AX650X_SDHCI_VENDOR_BASE, + &s->vendor_reg_array->mem); + sysbus_pass_irq(sbd, sdhci_sbd); +} + +static const VMStateDescription vmstate_ax650x_sdhci = { + .name = TYPE_AX650X_SDHCI, + .version_id = 1, + .minimum_version_id = 1, + .fields = (const VMStateField[]) { + VMSTATE_UINT8_ARRAY(pointer_regs, AX650XSDHCIState, 2), + VMSTATE_UINT8_ARRAY(vendor_regs, AX650XSDHCIState, + AX650X_SDHCI_VENDOR_SIZE), + VMSTATE_BOOL(emmc_reset_asserted, AX650XSDHCIState), + VMSTATE_END_OF_LIST(), + }, +}; + +static void ax650x_sdhci_init(Object *obj) +{ + AX650XSDHCIState *s = AX650X_SDHCI(obj); + + object_initialize_child(obj, "generic-sdhci", &s->sdhci, + TYPE_SYSBUS_SDHCI); + s->pointer_reg_array = + register_init_block8(DEVICE(obj), ax650x_pointer_regs_info, + ARRAY_SIZE(ax650x_pointer_regs_info), + s->pointer_regs_info, s->pointer_regs, + &ax650x_register_bank_ops, false, + sizeof(s->pointer_regs)); + s->vendor_reg_array = + register_init_block8(DEVICE(obj), ax650x_vendor_regs_info, + ARRAY_SIZE(ax650x_vendor_regs_info), + s->vendor_regs_info, s->vendor_regs, + &ax650x_register_bank_ops, false, + sizeof(s->vendor_regs)); +} + +static void ax650x_sdhci_class_init(ObjectClass *oc, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(oc); + + dc->desc = "Axera AX650X SD/eMMC Host Controller"; + dc->realize = ax650x_sdhci_realize; + device_class_set_legacy_reset(dc, ax650x_sdhci_reset); + dc->vmsd = &vmstate_ax650x_sdhci; +} + +SDBus *ax650x_sdhci_get_bus(AX650XSDHCIState *s) +{ + return &s->sdhci.sdbus; +} + +static const TypeInfo ax650x_sdhci_info = { + .name = TYPE_AX650X_SDHCI, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(AX650XSDHCIState), + .instance_init = ax650x_sdhci_init, + .class_init = ax650x_sdhci_class_init, +}; + +static void ax650x_sdhci_register_types(void) +{ + type_register_static(&ax650x_sdhci_info); +} + +type_init(ax650x_sdhci_register_types) diff --git a/hw/sd/meson.build b/hw/sd/meson.build index b637c06c87ee..bf3ec8b98458 100644 --- a/hw/sd/meson.build +++ b/hw/sd/meson.build @@ -15,3 +15,4 @@ 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')) +system_ss.add(when: 'CONFIG_AX650X_SDHCI', if_true: files('ax650x-sdhci.c')) diff --git a/include/hw/sd/ax650x-sdhci.h b/include/hw/sd/ax650x-sdhci.h new file mode 100644 index 000000000000..be4b5083186b --- /dev/null +++ b/include/hw/sd/ax650x-sdhci.h @@ -0,0 +1,75 @@ +/* + * Axera AX650X SDHCI controller + * + * Copyright (c) 2026 Zevorn + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef HW_SD_AX650X_SDHCI_H +#define HW_SD_AX650X_SDHCI_H + +#include "hw/core/register.h" +#include "hw/sd/sdhci.h" + +#define TYPE_AX650X_SDHCI "ax650x-sdhci" +OBJECT_DECLARE_SIMPLE_TYPE(AX650XSDHCIState, AX650X_SDHCI) + +#define AX650X_SDHCI_REG_SIZE 0x600 +#define AX650X_SDHCI_VENDOR_BASE 0x100 +#define AX650X_SDHCI_VENDOR_SIZE 0x500 +#define AX650X_SDHCI_VENDOR_PTR 0x0e8 +#define AX650X_SDHCI_VENDOR_PTR_VALUE 0x500 + +#define AX650X_SDHCI_PHY_CNFG 0x300 +#define AX650X_SDHCI_PHY_CMDPAD_CNFG 0x304 +#define AX650X_SDHCI_PHY_DATAPAD_CNFG 0x306 +#define AX650X_SDHCI_PHY_CLKPAD_CNFG 0x308 +#define AX650X_SDHCI_PHY_STBPAD_CNFG 0x30a +#define AX650X_SDHCI_PHY_RSTNPAD_CNFG 0x30c +#define AX650X_SDHCI_PHY_COMMDL_CNFG 0x31c +#define AX650X_SDHCI_PHY_SDCLKDL_CNFG 0x31d +#define AX650X_SDHCI_PHY_SDCLKDL_DC 0x31e +#define AX650X_SDHCI_PHY_SMPLDL_CNFG 0x320 +#define AX650X_SDHCI_PHY_ATDL_CNFG 0x321 +#define AX650X_SDHCI_PHY_DLL_CTRL 0x324 +#define AX650X_SDHCI_PHY_DLL_CNFG1 0x325 +#define AX650X_SDHCI_PHY_DLL_CNFG2 0x326 +#define AX650X_SDHCI_PHY_DLLDL_CNFG 0x328 +#define AX650X_SDHCI_PHY_DLL_OFFSET 0x329 +#define AX650X_SDHCI_PHY_DLLLBT_CNFG 0x32c +#define AX650X_SDHCI_PHY_DLL_STATUS 0x32e +#define AX650X_SDHCI_PHY_DLLDBG_MLKDC 0x330 +#define AX650X_SDHCI_PHY_DLLDBG_SLKDC 0x332 +#define AX650X_SDHCI_EMMC_CTRL 0x52c + +#define AX650X_SDHCI_PHY_PWRGOOD BIT(1) +#define AX650X_SDHCI_PHY_RSTN BIT(0) +#define AX650X_SDHCI_DLL_EN BIT(0) +#define AX650X_SDHCI_DLL_ERROR BIT(1) +#define AX650X_SDHCI_DLL_LOCKED BIT(0) +#define AX650X_SDHCI_CARD_IS_EMMC BIT(0) +#define AX650X_SDHCI_EMMC_RST_N BIT(2) +#define AX650X_SDHCI_EMMC_RST_N_OE BIT(3) +#define AX650X_SDHCI_ENH_STROBE_EN BIT(8) + +struct AX650XSDHCIState { + SysBusDevice parent_obj; + + MemoryRegion container; + SDHCIState sdhci; + + RegisterInfo pointer_regs_info[2]; + uint8_t pointer_regs[2]; + RegisterInfoArray *pointer_reg_array; + + RegisterInfo vendor_regs_info[AX650X_SDHCI_VENDOR_SIZE]; + uint8_t vendor_regs[AX650X_SDHCI_VENDOR_SIZE]; + RegisterInfoArray *vendor_reg_array; + + bool emmc_reset_asserted; +}; + +SDBus *ax650x_sdhci_get_bus(AX650XSDHCIState *s); + +#endif From 6b56b705208bf5981ef8f5940b39a9ab48ce0d3f Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 21:49:19 +0800 Subject: [PATCH 03/14] hw/arm: Wire AX650X Pyramid eMMC Instantiate the AX650X SDHCI wrapper at the hardware address, connect SPI 93, and attach an eMMC backend supplied through if=sd. Describe the controller, clocks, capabilities, and PHY tuning properties in the generated device tree. Exercise the vendor, PHY, DLL and eMMC control registers, reset state, SDHCI capabilities, interrupt routing, and programmed-I/O block transfers with qtest. Signed-off-by: Chao Liu --- hw/arm/ax650x.c | 93 +++++++++++++++ tests/qtest/ax650x-pyramid-test.c | 185 +++++++++++++++++++++++++++++- 2 files changed, 277 insertions(+), 1 deletion(-) diff --git a/hw/arm/ax650x.c b/hw/arm/ax650x.c index 7cb601547147..7e5f2fbae25b 100644 --- a/hw/arm/ax650x.c +++ b/hw/arm/ax650x.c @@ -20,7 +20,10 @@ #include "hw/core/qdev-properties.h" #include "hw/core/sysbus.h" #include "hw/intc/arm_gic.h" +#include "hw/sd/ax650x-sdhci.h" +#include "hw/sd/sd.h" #include "system/address-spaces.h" +#include "system/blockdev.h" #include "system/device_tree.h" #include "system/system.h" #include "target/arm/cpu.h" @@ -52,6 +55,11 @@ OBJECT_DECLARE_SIMPLE_TYPE(AX650XPyramidState, AX650X_PYRAMID_MACHINE) #define AX650X_UART_CLOCK_HZ 200000000 #define AX650X_UART_BAUDBASE (AX650X_UART_CLOCK_HZ / 16) +#define AX650X_EMMC_BASE 0x28000000 +#define AX650X_EMMC_SIZE 0x600 +#define AX650X_EMMC_IRQ 93 +#define AX650X_EMMC_CLOCK_HZ 200000000 + #define AX650X_TIMER_CLOCK_HZ 24000000 #define AX650X_PMU_IRQ_BASE 80 #define AX650X_GIC_MAINT_PPI 9 @@ -161,6 +169,28 @@ static void ax650x_create_gic(AX650XPyramidState *s) } } +static void ax650x_create_emmc(AX650XPyramidState *s) +{ + DeviceState *host = qdev_new(TYPE_AX650X_SDHCI); + SysBusDevice *sbd = SYS_BUS_DEVICE(host); + DriveInfo *dinfo = drive_get(IF_SD, 0, 0); + + sysbus_realize_and_unref(sbd, &error_fatal); + sysbus_mmio_map(sbd, 0, AX650X_EMMC_BASE); + sysbus_connect_irq(sbd, 0, + qdev_get_gpio_in(s->gic, AX650X_EMMC_IRQ)); + + if (dinfo) { + DeviceState *card = qdev_new(TYPE_EMMC); + + qdev_prop_set_drive_err(card, "drive", blk_by_legacy_dinfo(dinfo), + &error_fatal); + qdev_realize_and_unref(card, + BUS(ax650x_sdhci_get_bus(AX650X_SDHCI(host))), + &error_fatal); + } +} + static void ax650x_create_fdt(AX650XPyramidState *s) { MachineState *machine = MACHINE(s); @@ -168,8 +198,11 @@ static void ax650x_create_fdt(AX650XPyramidState *s) uint32_t gic_phandle; uint32_t uart_baud_phandle; uint32_t uart_apb_phandle; + uint32_t emmc_clock_phandle; const char clock_names[] = "baudclk\0apb_pclk"; + const char emmc_clock_names[] = "aclk\0hclk\0cardclk"; const char uart_path[] = "/soc/ax_uart@2016000"; + const char emmc_path[] = "/soc/sdhc@28000000"; s->fdt = create_device_tree(&s->fdt_size); if (!s->fdt) { @@ -305,6 +338,16 @@ static void ax650x_create_fdt(AX650XPyramidState *s) qemu_fdt_setprop_cell(s->fdt, "/uart-apb-clock", "phandle", uart_apb_phandle); + qemu_fdt_add_subnode(s->fdt, "/emmc-clock"); + qemu_fdt_setprop_string(s->fdt, "/emmc-clock", "compatible", + "fixed-clock"); + qemu_fdt_setprop_cell(s->fdt, "/emmc-clock", "#clock-cells", 0); + qemu_fdt_setprop_cell(s->fdt, "/emmc-clock", "clock-frequency", + AX650X_EMMC_CLOCK_HZ); + emmc_clock_phandle = qemu_fdt_alloc_phandle(s->fdt); + qemu_fdt_setprop_cell(s->fdt, "/emmc-clock", "phandle", + emmc_clock_phandle); + qemu_fdt_add_subnode(s->fdt, "/soc"); qemu_fdt_setprop_string(s->fdt, "/soc", "compatible", "simple-bus"); qemu_fdt_setprop_cell(s->fdt, "/soc", "#address-cells", 2); @@ -328,6 +371,55 @@ static void ax650x_create_fdt(AX650XPyramidState *s) GIC_FDT_IRQ_TYPE_SPI, AX650X_UART0_IRQ, GIC_FDT_IRQ_FLAGS_LEVEL_HI); qemu_fdt_setprop_string(s->fdt, uart_path, "status", "okay"); + + qemu_fdt_add_subnode(s->fdt, emmc_path); + qemu_fdt_setprop_string(s->fdt, emmc_path, "compatible", + "axera,sdhc-ax650"); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "#address-cells", 2); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "#size-cells", 2); + qemu_fdt_setprop_sized_cells(s->fdt, emmc_path, "reg", + 2, AX650X_EMMC_BASE, + 2, AX650X_EMMC_SIZE); + qemu_fdt_setprop_cells(s->fdt, emmc_path, "interrupts", + GIC_FDT_IRQ_TYPE_SPI, AX650X_EMMC_IRQ, + GIC_FDT_IRQ_FLAGS_LEVEL_HI); + qemu_fdt_setprop_cells(s->fdt, emmc_path, "clocks", + emmc_clock_phandle, emmc_clock_phandle, + emmc_clock_phandle); + qemu_fdt_setprop(s->fdt, emmc_path, "clock-names", + emmc_clock_names, sizeof(emmc_clock_names)); + qemu_fdt_setprop_cells(s->fdt, emmc_path, "sdhci-caps-mask", + 0x2, 0x03200000); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "bus-width", 8); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "max-frequency", + AX650X_EMMC_CLOCK_HZ); + qemu_fdt_setprop(s->fdt, emmc_path, "cap-mmc-hw-reset", NULL, 0); + qemu_fdt_setprop(s->fdt, emmc_path, "cap-mmc-highspeed", NULL, 0); + qemu_fdt_setprop(s->fdt, emmc_path, "mmc-hs200-1_8v", NULL, 0); + qemu_fdt_setprop(s->fdt, emmc_path, "mmc-hs400-1_8v", NULL, 0); + qemu_fdt_setprop(s->fdt, emmc_path, "mmc-hs400-enhanced-strobe", + NULL, 0); + qemu_fdt_setprop(s->fdt, emmc_path, "no-sdio", NULL, 0); + qemu_fdt_setprop(s->fdt, emmc_path, "no-sd", NULL, 0); + qemu_fdt_setprop(s->fdt, emmc_path, "non-removable", NULL, 0); + qemu_fdt_setprop(s->fdt, emmc_path, "disable-wp", NULL, 0); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-cnfg", 0x00cc0000); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-cmdpad-cnfg", + 0x0449); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-datapad-cnfg", + 0x0449); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-clkpad-cnfg", + 0x0440); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-stbpad-cnfg", + 0x0451); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-rstnpad-cnfg", + 0x0449); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-commdl-cnfg", 0); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-sdclkdl-cnfg", 1); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-sdclkdl-dc", 0x7f); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-smpldl-cnfg", 0xc); + qemu_fdt_setprop_cell(s->fdt, emmc_path, "axera,phy-atdl-cnfg", 0xc); + qemu_fdt_setprop_string(s->fdt, emmc_path, "status", "okay"); } static void *ax650x_get_dtb(const struct arm_boot_info *binfo, int *fdt_size) @@ -352,6 +444,7 @@ static void ax650x_pyramid_init(MachineState *machine) memory_region_add_subregion(sysmem, AX650X_RAM_BASE, machine->ram); ax650x_create_cpus(s); ax650x_create_gic(s); + ax650x_create_emmc(s); serial_mm_init(sysmem, AX650X_UART0_BASE, 2, qdev_get_gpio_in(s->gic, AX650X_UART0_IRQ), diff --git a/tests/qtest/ax650x-pyramid-test.c b/tests/qtest/ax650x-pyramid-test.c index f53eb30164c0..b5cc1ecc814b 100644 --- a/tests/qtest/ax650x-pyramid-test.c +++ b/tests/qtest/ax650x-pyramid-test.c @@ -8,10 +8,12 @@ #include "qemu/osdep.h" #include "qemu/units.h" +#include "hw/sd/ax650x-sdhci.h" #include "qobject/qdict.h" #include "qobject/qlist.h" #include "qobject/qnum.h" #include "libqtest.h" +#include "libqos/sdhci-cmd.h" #define AX650X_NUM_CPUS 8 @@ -34,6 +36,21 @@ #define AX650X_UART_LSR_TEMT 0x40 #define AX650X_UART0_INTID (32 + 135) +#define AX650X_EMMC_BASE 0x28000000 +#define AX650X_EMMC_INTID (32 + 93) +#define AX650X_EMMC_TEST_IMAGE_SIZE (1 * MiB) +#define AX650X_EMMC_BLOCK_SIZE 512 + +#define SDHC_NORINTSTS 0x30 +#define SDHC_NORINTSTSEN 0x34 +#define SDHC_NORINTSIGEN 0x38 +#define SDHC_NIS_CMDCMP BIT(0) +#define SDHC_AX650X_HCVER 0x2402 +#define SDHC_EMMC_SEND_OP_COND (1 << 8) +#define SDHC_EMMC_SET_RELATIVE_ADDR (3 << 8) + +static char *emmc_path; + static QTestState *ax650x_pyramid_start(void) { return qtest_init("-machine ax650x-pyramid -accel qtest -display none"); @@ -142,14 +159,180 @@ static void test_uart_irq_and_reset(void) qtest_quit(qts); } +static void test_emmc_registers_and_reset(void) +{ + QTestState *qts = ax650x_pyramid_start(); + uint64_t base = AX650X_EMMC_BASE; + uint64_t capabilities; + + g_assert_cmphex(qtest_readw(qts, base + AX650X_SDHCI_VENDOR_PTR), ==, + AX650X_SDHCI_VENDOR_PTR_VALUE); + capabilities = qtest_readq(qts, base + SDHC_CAPAB); + g_assert_cmphex(capabilities & BIT_ULL(18), ==, BIT_ULL(18)); + g_assert_cmphex(qtest_readw(qts, base + SDHC_HCVER), ==, + SDHC_AX650X_HCVER); + + g_assert_cmphex(qtest_readl(qts, base + AX650X_SDHCI_PHY_CNFG), ==, + AX650X_SDHCI_PHY_PWRGOOD); + qtest_writel(qts, base + AX650X_SDHCI_PHY_CNFG, 0x00cc0001); + g_assert_cmphex(qtest_readl(qts, base + AX650X_SDHCI_PHY_CNFG), ==, + 0x00cc0003); + + qtest_writew(qts, base + AX650X_SDHCI_PHY_CMDPAD_CNFG, 0x0449); + g_assert_cmphex(qtest_readw(qts, + base + AX650X_SDHCI_PHY_CMDPAD_CNFG), ==, + 0x0449); + qtest_writeb(qts, base + AX650X_SDHCI_PHY_DLL_CTRL, + AX650X_SDHCI_DLL_EN); + g_assert_cmphex(qtest_readb(qts, + base + AX650X_SDHCI_PHY_DLL_STATUS), ==, + AX650X_SDHCI_DLL_LOCKED); + qtest_writeb(qts, base + AX650X_SDHCI_PHY_DLL_CTRL, 0); + g_assert_cmphex(qtest_readb(qts, + base + AX650X_SDHCI_PHY_DLL_STATUS), ==, 0); + + qtest_writeb(qts, base + AX650X_SDHCI_PHY_DLLDBG_MLKDC, 0xff); + qtest_writeb(qts, base + AX650X_SDHCI_PHY_DLLDBG_SLKDC, 0xff); + g_assert_cmphex(qtest_readb(qts, + base + AX650X_SDHCI_PHY_DLLDBG_MLKDC), ==, + 98); + g_assert_cmphex(qtest_readb(qts, + base + AX650X_SDHCI_PHY_DLLDBG_SLKDC), ==, + 24); + + qtest_writew(qts, base + AX650X_SDHCI_EMMC_CTRL, 0xffff); + g_assert_cmphex(qtest_readw(qts, base + AX650X_SDHCI_EMMC_CTRL), ==, + AX650X_SDHCI_CARD_IS_EMMC | + AX650X_SDHCI_EMMC_RST_N | + AX650X_SDHCI_EMMC_RST_N_OE | + AX650X_SDHCI_ENH_STROBE_EN); + + qtest_system_reset(qts); + g_assert_cmphex(qtest_readl(qts, base + AX650X_SDHCI_PHY_CNFG), ==, + AX650X_SDHCI_PHY_PWRGOOD); + g_assert_cmphex(qtest_readw(qts, + base + AX650X_SDHCI_PHY_CMDPAD_CNFG), ==, 0); + g_assert_cmphex(qtest_readb(qts, + base + AX650X_SDHCI_PHY_DLL_STATUS), ==, 0); + g_assert_cmphex(qtest_readw(qts, base + AX650X_SDHCI_EMMC_CTRL), ==, 0); + g_assert_cmphex(qtest_readw(qts, base + AX650X_SDHCI_VENDOR_PTR), ==, + AX650X_SDHCI_VENDOR_PTR_VALUE); + + qtest_quit(qts); +} + +static QTestState *ax650x_pyramid_start_with_emmc(void) +{ + QTestState *qts; + uint64_t base = AX650X_EMMC_BASE; + + qts = qtest_initf("-machine ax650x-pyramid -accel qtest -display none " + "-drive file=%s,if=sd,format=raw,auto-read-only=off", + emmc_path); + qtest_writeb(qts, base + SDHC_SWRST, SDHC_RESET_ALL); + qtest_writew(qts, base + SDHC_CLKCON, + SDHC_CLOCK_SDCLK_EN | SDHC_CLOCK_INT_EN); + + qtest_writel(qts, base + SDHC_NORINTSTSEN, SDHC_NIS_CMDCMP); + qtest_writel(qts, base + SDHC_NORINTSIGEN, SDHC_NIS_CMDCMP); + sdhci_cmd_regs(qts, base, 0, 0, 0, 0, 0); + + sdhci_cmd_regs(qts, base, 0, 0, 0x40ff8000, 0, + SDHC_EMMC_SEND_OP_COND); + sdhci_cmd_regs(qts, base, 0, 0, 0, 0, SDHC_ALL_SEND_CID); + sdhci_cmd_regs(qts, base, 0, 0, 1 << 16, 0, + SDHC_EMMC_SET_RELATIVE_ADDR | SDHC_CMD_RESPONSE); + sdhci_cmd_regs(qts, base, 0, 0, 1 << 16, 0, + SDHC_SELECT_DESELECT_CARD); + + return qts; +} + +static void test_emmc_block_io_and_irq(void) +{ + uint8_t source[AX650X_EMMC_BLOCK_SIZE]; + uint8_t written[AX650X_EMMC_BLOCK_SIZE]; + uint8_t readback[AX650X_EMMC_BLOCK_SIZE]; + QTestState *qts; + uint64_t pending_addr; + uint32_t pending_mask; + int fd; + ssize_t ret; + + for (unsigned int i = 0; i < sizeof(source); i++) { + source[i] = i % 127 + 1; + written[i] = (i * 3) % 127 + 1; + } + fd = open(emmc_path, O_WRONLY); + g_assert_cmpint(fd, >=, 0); + ret = pwrite(fd, source, sizeof(source), 0); + g_assert_cmpint(ret, ==, sizeof(source)); + close(fd); + + qts = ax650x_pyramid_start_with_emmc(); + g_assert_cmphex(qtest_readw(qts, + AX650X_EMMC_BASE + SDHC_NORINTSTS) & + SDHC_NIS_CMDCMP, ==, SDHC_NIS_CMDCMP); + pending_addr = AX650X_GIC_DIST_BASE + AX650X_GICD_ISPENDR + + (AX650X_EMMC_INTID / 32) * sizeof(uint32_t); + pending_mask = 1U << (AX650X_EMMC_INTID % 32); + g_assert_cmphex(qtest_readl(qts, pending_addr) & pending_mask, ==, + pending_mask); + + ret = sdhci_read_cmd(qts, AX650X_EMMC_BASE, (char *)readback, + sizeof(readback)); + g_assert_cmpint(ret, ==, sizeof(readback)); + g_assert_cmpmem(readback, sizeof(readback), source, sizeof(source)); + + sdhci_write_cmd(qts, AX650X_EMMC_BASE, (char *)written, + sizeof(written), AX650X_EMMC_BLOCK_SIZE); + qtest_quit(qts); + + fd = open(emmc_path, O_RDONLY); + g_assert_cmpint(fd, >=, 0); + ret = pread(fd, readback, sizeof(readback), 0); + g_assert_cmpint(ret, ==, sizeof(readback)); + close(fd); + g_assert_cmpmem(readback, sizeof(readback), written, sizeof(written)); +} + +static void emmc_drive_create(void) +{ + GError *error = NULL; + int fd; + + fd = g_file_open_tmp("ax650x-emmc-XXXXXX", &emmc_path, &error); + if (fd < 0) { + g_error("unable to create eMMC image: %s", error->message); + } + g_assert_nonnull(emmc_path); + g_assert_cmpint(ftruncate(fd, AX650X_EMMC_TEST_IMAGE_SIZE), ==, 0); + close(fd); +} + +static void emmc_drive_destroy(void) +{ + unlink(emmc_path); + g_free(emmc_path); +} + int main(int argc, char **argv) { + int ret; + + emmc_drive_create(); g_test_init(&argc, &argv, NULL); qtest_add_func("ax650x-pyramid/cpu-topology", test_cpu_topology); qtest_add_func("ax650x-pyramid/memory-and-gic", test_memory_and_gic); qtest_add_func("ax650x-pyramid/uart-irq-and-reset", test_uart_irq_and_reset); + qtest_add_func("ax650x-pyramid/emmc-registers-and-reset", + test_emmc_registers_and_reset); + qtest_add_func("ax650x-pyramid/emmc-block-io-and-irq", + test_emmc_block_io_and_irq); - return g_test_run(); + ret = g_test_run(); + emmc_drive_destroy(); + return ret; } From f37a05de0dfedad4b393c405debfb1c265b7e0b4 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 21:49:19 +0800 Subject: [PATCH 04/14] hw/arm: Model AX650X UART extension window Add the AX650X UART extension register window and verify its reset, read and write behavior with qtest. Signed-off-by: Chao Liu --- hw/arm/ax650x.c | 51 +++++++++++++++++++++++++++++++ tests/qtest/ax650x-pyramid-test.c | 18 +++++++++++ 2 files changed, 69 insertions(+) diff --git a/hw/arm/ax650x.c b/hw/arm/ax650x.c index 7e5f2fbae25b..9b4732526533 100644 --- a/hw/arm/ax650x.c +++ b/hw/arm/ax650x.c @@ -51,6 +51,11 @@ OBJECT_DECLARE_SIMPLE_TYPE(AX650XPyramidState, AX650X_PYRAMID_MACHINE) #define AX650X_UART0_BASE 0x02016000 #define AX650X_UART0_SIZE 0x400 +#define AX650X_UART_STD_SIZE 0x20 +#define AX650X_UART_USR 0x7c +#define AX650X_UART_UCV 0xf8 +#define AX650X_UART_USR_TFNF BIT(1) +#define AX650X_UART_USR_TFE BIT(2) #define AX650X_UART0_IRQ 135 #define AX650X_UART_CLOCK_HZ 200000000 #define AX650X_UART_BAUDBASE (AX650X_UART_CLOCK_HZ / 16) @@ -71,11 +76,51 @@ struct AX650XPyramidState { ARMCPU *cpus[AX650X_NUM_CPUS]; DeviceState *gic; MemoryRegion gic_cpu_alias; + MemoryRegion uart0_ext; struct arm_boot_info bootinfo; void *fdt; int fdt_size; }; +static uint64_t ax650x_uart_ext_read(void *opaque, hwaddr offset, + unsigned int size) +{ + hwaddr reg = AX650X_UART_STD_SIZE + offset; + + switch (reg) { + case AX650X_UART_USR: + return AX650X_UART_USR_TFNF | AX650X_UART_USR_TFE; + case AX650X_UART_UCV: + /* + * The hardware component revision is not documented. Zero selects + * the AXERA driver's defined fallback for a UART without DesignWare + * additional features. + */ + return 0; + default: + return 0; + } +} + +static void ax650x_uart_ext_write(void *opaque, hwaddr offset, + uint64_t value, unsigned int size) +{ +} + +static const MemoryRegionOps ax650x_uart_ext_ops = { + .read = ax650x_uart_ext_read, + .write = ax650x_uart_ext_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid = { + .min_access_size = 1, + .max_access_size = 4, + }, + .impl = { + .min_access_size = 1, + .max_access_size = 4, + }, +}; + static uint64_t ax650x_cpu_mpidr(unsigned int cpu) { return (uint64_t)cpu << 8; @@ -449,6 +494,12 @@ static void ax650x_pyramid_init(MachineState *machine) serial_mm_init(sysmem, AX650X_UART0_BASE, 2, qdev_get_gpio_in(s->gic, AX650X_UART0_IRQ), AX650X_UART_BAUDBASE, serial_hd(0), DEVICE_LITTLE_ENDIAN); + memory_region_init_io(&s->uart0_ext, OBJECT(s), &ax650x_uart_ext_ops, s, + "ax650x.uart0-ext", + AX650X_UART0_SIZE - AX650X_UART_STD_SIZE); + memory_region_add_subregion(sysmem, + AX650X_UART0_BASE + AX650X_UART_STD_SIZE, + &s->uart0_ext); ax650x_create_fdt(s); diff --git a/tests/qtest/ax650x-pyramid-test.c b/tests/qtest/ax650x-pyramid-test.c index b5cc1ecc814b..09d741f8ef3c 100644 --- a/tests/qtest/ax650x-pyramid-test.c +++ b/tests/qtest/ax650x-pyramid-test.c @@ -31,9 +31,13 @@ #define AX650X_UART_REGSHIFT 2 #define AX650X_UART_IER (1 << AX650X_UART_REGSHIFT) #define AX650X_UART_LSR (5 << AX650X_UART_REGSHIFT) +#define AX650X_UART_USR 0x7c +#define AX650X_UART_UCV 0xf8 #define AX650X_UART_IER_THRI 0x02 #define AX650X_UART_LSR_THRE 0x20 #define AX650X_UART_LSR_TEMT 0x40 +#define AX650X_UART_USR_TFNF BIT(1) +#define AX650X_UART_USR_TFE BIT(2) #define AX650X_UART0_INTID (32 + 135) #define AX650X_EMMC_BASE 0x28000000 @@ -159,6 +163,18 @@ static void test_uart_irq_and_reset(void) qtest_quit(qts); } +static void test_uart_extension_registers(void) +{ + QTestState *qts = ax650x_pyramid_start(); + + g_assert_cmphex(qtest_readl(qts, AX650X_UART0_BASE + AX650X_UART_USR), + ==, AX650X_UART_USR_TFNF | AX650X_UART_USR_TFE); + g_assert_cmphex(qtest_readl(qts, AX650X_UART0_BASE + AX650X_UART_UCV), + ==, 0); + + qtest_quit(qts); +} + static void test_emmc_registers_and_reset(void) { QTestState *qts = ax650x_pyramid_start(); @@ -327,6 +343,8 @@ int main(int argc, char **argv) qtest_add_func("ax650x-pyramid/memory-and-gic", test_memory_and_gic); qtest_add_func("ax650x-pyramid/uart-irq-and-reset", test_uart_irq_and_reset); + qtest_add_func("ax650x-pyramid/uart-extension-registers", + test_uart_extension_registers); qtest_add_func("ax650x-pyramid/emmc-registers-and-reset", test_emmc_registers_and_reset); qtest_add_func("ax650x-pyramid/emmc-block-io-and-irq", From ec3fdc7fbd6593878727a1ee31b0817439867c99 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 21:49:19 +0800 Subject: [PATCH 05/14] hw/sd: Advertise AX650X 64-bit ADMA support Expose the AX650X SDHCI 64-bit ADMA capability and verify the capability bit with qtest. Signed-off-by: Chao Liu --- hw/sd/ax650x-sdhci.c | 4 +++- tests/qtest/ax650x-pyramid-test.c | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/hw/sd/ax650x-sdhci.c b/hw/sd/ax650x-sdhci.c index c983078daf79..ae6dcbd4515d 100644 --- a/hw/sd/ax650x-sdhci.c +++ b/hw/sd/ax650x-sdhci.c @@ -162,7 +162,9 @@ static void ax650x_sdhci_realize(DeviceState *dev, Error **errp) qdev_prop_set_uint8(DEVICE(&s->sdhci), "sd-spec-version", 3); qdev_prop_set_uint8(DEVICE(&s->sdhci), "uhs", UHS_I); qdev_prop_set_uint64(DEVICE(&s->sdhci), "capareg", - SDHC_CAPAB_REG_DEFAULT | BIT_ULL(18)); + SDHC_CAPAB_REG_DEFAULT | + R_SDHC_CAPAB_EMBEDDED_8BIT_MASK | + R_SDHC_CAPAB_BUS64BIT_MASK); if (!sysbus_realize(sdhci_sbd, errp)) { return; } diff --git a/tests/qtest/ax650x-pyramid-test.c b/tests/qtest/ax650x-pyramid-test.c index 09d741f8ef3c..73fce6d90dfc 100644 --- a/tests/qtest/ax650x-pyramid-test.c +++ b/tests/qtest/ax650x-pyramid-test.c @@ -185,6 +185,7 @@ static void test_emmc_registers_and_reset(void) AX650X_SDHCI_VENDOR_PTR_VALUE); capabilities = qtest_readq(qts, base + SDHC_CAPAB); g_assert_cmphex(capabilities & BIT_ULL(18), ==, BIT_ULL(18)); + g_assert_cmphex(capabilities & BIT_ULL(28), ==, BIT_ULL(28)); g_assert_cmphex(qtest_readw(qts, base + SDHC_HCVER), ==, SDHC_AX650X_HCVER); From c0a4aa1600dfd3d7d90782ee93f20b0fbdd030f6 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 21:49:19 +0800 Subject: [PATCH 06/14] hw/sd: Implement SDHCI Auto CMD23 Implement SDHCI Auto CMD23 handling and exercise the AX650X command path with qtest. Signed-off-by: Chao Liu --- hw/sd/sdhci-internal.h | 2 +- hw/sd/sdhci.c | 31 +++++++++++++++++++ tests/qtest/ax650x-pyramid-test.c | 49 +++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/hw/sd/sdhci-internal.h b/hw/sd/sdhci-internal.h index b7457199c1ed..6bc6b48c4e65 100644 --- a/hw/sd/sdhci-internal.h +++ b/hw/sd/sdhci-internal.h @@ -46,7 +46,7 @@ #define SDHC_TRNS_ACMD23 0x0008 /* since v3 */ #define SDHC_TRNS_READ 0x0010 #define SDHC_TRNS_MULTI 0x0020 -#define SDHC_TRNMOD_MASK 0x0037 +#define SDHC_TRNMOD_MASK 0x003f /* R/W Command Register 0x0 */ #define SDHC_CMDREG 0x0E diff --git a/hw/sd/sdhci.c b/hw/sd/sdhci.c index 76c94642227f..67e7e600128d 100644 --- a/hw/sd/sdhci.c +++ b/hw/sd/sdhci.c @@ -336,6 +336,30 @@ static void sdhci_data_transfer(void *opaque); #define BLOCK_SIZE_MASK (4 * KiB - 1) +static bool sdhci_send_auto_cmd23(SDHCIState *s) +{ + SDRequest request = { + .cmd = 23, + .arg = s->sdmasysad, + }; + uint8_t response[16]; + size_t rlen; + + trace_sdhci_send_command(request.cmd, request.arg); + rlen = sdbus_do_command(&s->sdbus, &request, response, sizeof(response)); + if (rlen == 4) { + return true; + } + + trace_sdhci_error("Auto CMD23 failed"); + s->acmd12errsts |= R_SDHC_ACMD12ERRSTS_TIMEOUT_ERR_MASK; + if (s->errintstsen & SDHC_EIS_CMD12ERR) { + s->errintsts |= SDHC_EIS_CMD12ERR; + s->norintsts |= SDHC_NIS_ERR; + } + return false; +} + static void sdhci_send_command(SDHCIState *s) { SDRequest request; @@ -348,6 +372,13 @@ static void sdhci_send_command(SDHCIState *s) request.cmd = s->cmdreg >> 8; request.arg = s->argument; + if ((s->trnmod & SDHC_TRNS_ACMD23) && + (s->cmdreg & SDHC_CMD_DATA_PRESENT) && + !sdhci_send_auto_cmd23(s)) { + sdhci_update_irq(s); + return; + } + trace_sdhci_send_command(request.cmd, request.arg); rlen = sdbus_do_command(&s->sdbus, &request, response, sizeof(response)); diff --git a/tests/qtest/ax650x-pyramid-test.c b/tests/qtest/ax650x-pyramid-test.c index 73fce6d90dfc..8b9b1a78bc53 100644 --- a/tests/qtest/ax650x-pyramid-test.c +++ b/tests/qtest/ax650x-pyramid-test.c @@ -48,10 +48,16 @@ #define SDHC_NORINTSTS 0x30 #define SDHC_NORINTSTSEN 0x34 #define SDHC_NORINTSIGEN 0x38 +#define SDHC_ARGUMENT2 0x00 +#define SDHC_TRNS_AUTO_CMD23 0x0008 #define SDHC_NIS_CMDCMP BIT(0) #define SDHC_AX650X_HCVER 0x2402 #define SDHC_EMMC_SEND_OP_COND (1 << 8) #define SDHC_EMMC_SET_RELATIVE_ADDR (3 << 8) +#define SDHC_EMMC_SEND_STATUS (13 << 8) +#define SDHC_R1_READY_FOR_DATA BIT(8) +#define SDHC_R1_CURRENT_STATE_MASK 0x1e00 +#define SDHC_R1_STATE_TRAN (4 << 9) static char *emmc_path; @@ -313,6 +319,47 @@ static void test_emmc_block_io_and_irq(void) g_assert_cmpmem(readback, sizeof(readback), written, sizeof(written)); } +static void test_emmc_auto_cmd23(void) +{ + uint8_t expected[AX650X_EMMC_BLOCK_SIZE]; + uint8_t readback[AX650X_EMMC_BLOCK_SIZE]; + QTestState *qts = ax650x_pyramid_start_with_emmc(); + uint32_t status; + int fd; + ssize_t ret; + + for (unsigned int i = 0; i < sizeof(expected); i++) { + expected[i] = (i * 5) % 251 + 1; + } + + qtest_writel(qts, AX650X_EMMC_BASE + SDHC_ARGUMENT2, 1); + sdhci_cmd_regs(qts, AX650X_EMMC_BASE, sizeof(expected), 1, + AX650X_EMMC_BLOCK_SIZE, + SDHC_TRNS_MULTI | SDHC_TRNS_AUTO_CMD23 | + SDHC_TRNS_BLK_CNT_EN, + SDHC_WRITE_MULTIPLE_BLOCK | SDHC_CMD_DATA_PRESENT); + for (unsigned int i = 0; i < sizeof(expected); i += sizeof(uint32_t)) { + qtest_writel(qts, AX650X_EMMC_BASE + SDHC_BDATA, + ldl_le_p(&expected[i])); + } + + sdhci_cmd_regs(qts, AX650X_EMMC_BASE, 0, 0, 1 << 16, 0, + SDHC_EMMC_SEND_STATUS | SDHC_CMD_RESPONSE); + status = qtest_readl(qts, AX650X_EMMC_BASE + SDHC_RSPREG0); + g_assert_cmphex(status & SDHC_R1_READY_FOR_DATA, ==, + SDHC_R1_READY_FOR_DATA); + g_assert_cmphex(status & SDHC_R1_CURRENT_STATE_MASK, ==, + SDHC_R1_STATE_TRAN); + qtest_quit(qts); + + fd = open(emmc_path, O_RDONLY); + g_assert_cmpint(fd, >=, 0); + ret = pread(fd, readback, sizeof(readback), AX650X_EMMC_BLOCK_SIZE); + g_assert_cmpint(ret, ==, sizeof(readback)); + close(fd); + g_assert_cmpmem(readback, sizeof(readback), expected, sizeof(expected)); +} + static void emmc_drive_create(void) { GError *error = NULL; @@ -350,6 +397,8 @@ int main(int argc, char **argv) test_emmc_registers_and_reset); qtest_add_func("ax650x-pyramid/emmc-block-io-and-irq", test_emmc_block_io_and_irq); + qtest_add_func("ax650x-pyramid/emmc-auto-cmd23", + test_emmc_auto_cmd23); ret = g_test_run(); emmc_drive_destroy(); From 9096675fe02d685cb411d5fa699022cc383930ab Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 21:49:19 +0800 Subject: [PATCH 07/14] docs/system/arm: Document AX650X Pyramid machine Signed-off-by: Chao Liu --- docs/system/arm/ax650x-pyramid.rst | 94 ++++++++++++++++++++++++++++++ docs/system/target-arm.rst | 1 + 2 files changed, 95 insertions(+) create mode 100644 docs/system/arm/ax650x-pyramid.rst diff --git a/docs/system/arm/ax650x-pyramid.rst b/docs/system/arm/ax650x-pyramid.rst new file mode 100644 index 000000000000..0715f5d8aecc --- /dev/null +++ b/docs/system/arm/ax650x-pyramid.rst @@ -0,0 +1,94 @@ +.. SPDX-License-Identifier: GPL-2.0-or-later + +M5Stack AI Pyramid (``ax650x-pyramid``) +======================================== + +Overview +-------- + +The ``ax650x-pyramid`` machine models the Linux boot subset of the M5Stack +AI Pyramid, whose runtime device tree identifies the SoC as +``axera,ax650x``. The model is intended for direct kernel boot with an +AX650X-enabled Arm64 Linux kernel. It does not model the product's AXERA +accelerators or firmware boot chain. + +The machine has a fixed topology matching the target system: eight Cortex-A55 +CPUs and 2 GiB of RAM starting at physical address ``0x100000000``. Other CPU +counts and memory sizes are rejected. + +Supported devices +----------------- + +The machine provides the following devices and architectural services: + +* eight Cortex-A55 CPUs, with MPIDRs ``0x000`` through ``0x700``; +* GIC-400 compatible GICv2 interrupt controller with virtualization + extensions; +* Arm generic timers running at 24 MHz; +* PSCI 1.0 using the SMC conduit; +* the AXERA UART0 console at ``0x02016000``, including the extension + registers needed by the AXERA 8250 driver; and +* an AX650X SDHCI/eMMC controller at ``0x28000000``, including the vendor PHY + and eMMC registers used during Linux probe. + +QEMU generates a minimal device tree containing only those implemented +devices. An eMMC backend supplied with ``if=sd`` is attached to the AX650X +controller. + +Direct Linux boot +----------------- + +The model requires an uncompressed Arm64 ``Image`` containing the AXERA UART +and SDHCI drivers. The following example boots an ext4 root filesystem from +partition 12 of an eMMC image that uses the target's fixed partition layout. +The image has no MBR or GPT, so the complete ``blkdevparts`` argument is +required:: + + $ KERNEL=/path/to/Image-5.15.73-axera + $ EMMC=/path/to/ax650x-ubuntu-22.04-emmc.raw + $ PARTS='mmcblk0:1536K(uboot),1536K(uboot_bk),1M(env),20M(param),6M(logo),1M(dtb),64M(kernel),1M(atf),1M(optee),1M(recovery_dtb),74M(recovery),30380032K(rootfs)' + $ qemu-system-aarch64 \ + -machine ax650x-pyramid \ + -accel tcg,thread=multi \ + -smp 8 -m 2G \ + -kernel "$KERNEL" \ + -append "console=ttyS0,115200n8 earlycon=uart8250,mmio32,0x2016000 root=/dev/mmcblk0p12 rootfstype=ext4 rw rootwait blkdevparts=$PARTS" \ + -drive file="$EMMC",if=sd,format=raw,snapshot=on \ + -serial mon:stdio -display none -no-reboot + +``snapshot=on`` keeps the reusable eMMC image unchanged. Remove it only when +persistent guest writes are intentional. + +The downstream model has been exercised with an AXERA Linux 5.15.73 kernel +and Ubuntu 22.04.5 LTS. Linux enumerates all eight CPUs, probes the eMMC using +64-bit ADMA, creates the twelve fixed partitions, and mounts +``/dev/mmcblk0p12`` as the writable root filesystem. + +Known limitations +----------------- + +* Only QEMU direct kernel boot is implemented. BootROM, SPL, U-Boot, Arm + Trusted Firmware and OP-TEE images are not loaded or executed. +* The AXERA DWMAC 4.10a Ethernet controllers are not modeled, so this machine + currently has no network interface. +* NPU, VDSP, RISC-V auxiliary cores, video, ISP, display, audio, USB, PCIe, + SATA, GPIO and the full clock, reset and power-management trees are not + modeled. +* The UART extension window implements the probe-time subset. The AXERA + driver can report a harmless capability mismatch because the UART component + version register intentionally selects its compatible fallback path. +* QEMU direct boot starts CPUs at EL2. The target firmware starts Linux at + EL1, so the vendor kernel can print a non-fatal GICv2 CPU-interface range + warning under QEMU. +* The SDHCI vendor bank implements the registers required for probe, reset and + data transfer. HS200/HS400 timing accuracy is not claimed. + +Running tests +------------- + +The board qtest covers CPU topology, RAM, GICv2, UART MMIO and IRQ behavior, +SDHCI vendor registers, reset, block I/O, eMMC IRQ routing, 64-bit ADMA +capability and Auto CMD23:: + + $ meson test -C build qemu:qtest-aarch64/ax650x-pyramid-test \ + --print-errorlogs diff --git a/docs/system/target-arm.rst b/docs/system/target-arm.rst index 8d36a9e180a2..be665576ab86 100644 --- a/docs/system/target-arm.rst +++ b/docs/system/target-arm.rst @@ -72,6 +72,7 @@ Board-specific documentation :maxdepth: 1 arm/max78000 + arm/ax650x-pyramid arm/integratorcp arm/mps2 arm/musca From 03ec0734ed7681afdb702912ebcddb83ac9b84c8 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 17:48:30 +0800 Subject: [PATCH 08/14] hw/gpio: add DesignWare APB GPIO model Signed-off-by: Chao Liu --- hw/gpio/Kconfig | 3 + hw/gpio/dw-apb-gpio.c | 132 ++++++++++++++++++++++++++++++++++ hw/gpio/meson.build | 1 + include/hw/gpio/dw-apb-gpio.h | 32 +++++++++ 4 files changed, 168 insertions(+) create mode 100644 hw/gpio/dw-apb-gpio.c create mode 100644 include/hw/gpio/dw-apb-gpio.h diff --git a/hw/gpio/Kconfig b/hw/gpio/Kconfig index 9d66fd5202f1..d8f12794f725 100644 --- a/hw/gpio/Kconfig +++ b/hw/gpio/Kconfig @@ -1,6 +1,9 @@ config PL061 bool +config DW_APB_GPIO + bool + config GPIO_KEY bool diff --git a/hw/gpio/dw-apb-gpio.c b/hw/gpio/dw-apb-gpio.c new file mode 100644 index 000000000000..2113aa2224a1 --- /dev/null +++ b/hw/gpio/dw-apb-gpio.c @@ -0,0 +1,132 @@ +/* + * Synopsys DesignWare APB GPIO + * + * This local model implements the single-port data, direction, and external + * value registers used by Linux gpio-dwapb. Interrupts and additional ports + * are deliberately outside its current contract. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "hw/core/registerfields.h" +#include "hw/gpio/dw-apb-gpio.h" +#include "migration/vmstate.h" +#include "qemu/module.h" + +REG32(SWPORTA_DR, 0x00) +REG32(SWPORTA_DDR, 0x04) +REG32(EXT_PORTA, 0x50) + +static void dw_apb_gpio_update(DWAPBGPIOState *s) +{ + uint32_t data = s->regs[R_SWPORTA_DR]; + uint32_t direction = s->regs[R_SWPORTA_DDR]; + + s->regs[R_EXT_PORTA] = (data & direction) | (s->input & ~direction); + for (unsigned int i = 0; i < DW_APB_GPIO_NR_PINS; i++) { + qemu_set_irq(s->output[i], + (direction & BIT(i)) && (data & BIT(i))); + } +} + +static void dw_apb_gpio_data_postw(RegisterInfo *reg, uint64_t value) +{ + dw_apb_gpio_update(DW_APB_GPIO(reg->opaque)); +} + +static const RegisterAccessInfo dw_apb_gpio_regs_info[] = { + { .name = "SWPORTA_DR", .addr = A_SWPORTA_DR, + .post_write = dw_apb_gpio_data_postw, + }, + { .name = "SWPORTA_DDR", .addr = A_SWPORTA_DDR, + .post_write = dw_apb_gpio_data_postw, + }, + { .name = "EXT_PORTA", .addr = A_EXT_PORTA, + .ro = MAKE_64BIT_MASK(0, 32), + }, +}; + +static const MemoryRegionOps dw_apb_gpio_ops = { + .read = register_read_memory, + .write = register_write_memory, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + .unaligned = false, + }, +}; + +static void dw_apb_gpio_set(void *opaque, int line, int level) +{ + DWAPBGPIOState *s = opaque; + + s->input = deposit32(s->input, line, 1, !!level); + dw_apb_gpio_update(s); +} + +static void dw_apb_gpio_reset(DeviceState *dev) +{ + DWAPBGPIOState *s = DW_APB_GPIO(dev); + + for (unsigned int i = 0; i < DW_APB_GPIO_NR_REGS; i++) { + register_reset(&s->regs_info[i]); + } + s->input = 0; + dw_apb_gpio_update(s); +} + +static int dw_apb_gpio_post_load(void *opaque, int version_id) +{ + dw_apb_gpio_update(DW_APB_GPIO(opaque)); + return 0; +} + +static const VMStateDescription vmstate_dw_apb_gpio = { + .name = TYPE_DW_APB_GPIO, + .version_id = 1, + .minimum_version_id = 1, + .post_load = dw_apb_gpio_post_load, + .fields = (const VMStateField[]) { + VMSTATE_UINT32_ARRAY(regs, DWAPBGPIOState, DW_APB_GPIO_NR_REGS), + VMSTATE_UINT32(input, DWAPBGPIOState), + VMSTATE_END_OF_LIST(), + }, +}; + +static void dw_apb_gpio_init(Object *obj) +{ + DWAPBGPIOState *s = DW_APB_GPIO(obj); + SysBusDevice *sbd = SYS_BUS_DEVICE(obj); + + s->reg_array = register_init_block32(DEVICE(obj), + dw_apb_gpio_regs_info, + ARRAY_SIZE(dw_apb_gpio_regs_info), + s->regs_info, s->regs, + &dw_apb_gpio_ops, false, + DW_APB_GPIO_MMIO_SIZE); + sysbus_init_mmio(sbd, &s->reg_array->mem); + qdev_init_gpio_in(DEVICE(obj), dw_apb_gpio_set, DW_APB_GPIO_NR_PINS); + qdev_init_gpio_out(DEVICE(obj), s->output, DW_APB_GPIO_NR_PINS); +} + +static void dw_apb_gpio_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + dc->desc = "Synopsys DesignWare APB GPIO"; + device_class_set_legacy_reset(dc, dw_apb_gpio_reset); + dc->vmsd = &vmstate_dw_apb_gpio; +} + +static const TypeInfo dw_apb_gpio_types[] = { + { + .name = TYPE_DW_APB_GPIO, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(DWAPBGPIOState), + .instance_init = dw_apb_gpio_init, + .class_init = dw_apb_gpio_class_init, + }, +}; +DEFINE_TYPES(dw_apb_gpio_types) diff --git a/hw/gpio/meson.build b/hw/gpio/meson.build index 301d57fa2699..fe890ab89e3b 100644 --- a/hw/gpio/meson.build +++ b/hw/gpio/meson.build @@ -1,4 +1,5 @@ system_ss.add(when: 'CONFIG_GPIO_KEY', if_true: files('gpio_key.c')) +system_ss.add(when: 'CONFIG_DW_APB_GPIO', if_true: files('dw-apb-gpio.c')) system_ss.add(when: 'CONFIG_GPIO_MPC8XXX', if_true: files('mpc8xxx.c')) system_ss.add(when: 'CONFIG_GPIO_PWR', if_true: files('gpio_pwr.c')) system_ss.add(when: 'CONFIG_PCA9552', if_true: files('pca9552.c')) diff --git a/include/hw/gpio/dw-apb-gpio.h b/include/hw/gpio/dw-apb-gpio.h new file mode 100644 index 000000000000..f3740f3b5c6c --- /dev/null +++ b/include/hw/gpio/dw-apb-gpio.h @@ -0,0 +1,32 @@ +/* + * Synopsys DesignWare APB GPIO + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef HW_GPIO_DW_APB_GPIO_H +#define HW_GPIO_DW_APB_GPIO_H + +#include "hw/core/irq.h" +#include "hw/core/register.h" +#include "hw/core/sysbus.h" + +#define TYPE_DW_APB_GPIO "dw-apb-gpio" +OBJECT_DECLARE_SIMPLE_TYPE(DWAPBGPIOState, DW_APB_GPIO) + +#define DW_APB_GPIO_NR_PINS 32 +#define DW_APB_GPIO_REG_SIZE 0x54 +#define DW_APB_GPIO_NR_REGS (DW_APB_GPIO_REG_SIZE / sizeof(uint32_t)) +#define DW_APB_GPIO_MMIO_SIZE 0x400 + +typedef struct DWAPBGPIOState { + SysBusDevice parent; + + uint32_t regs[DW_APB_GPIO_NR_REGS]; + RegisterInfo regs_info[DW_APB_GPIO_NR_REGS]; + RegisterInfoArray *reg_array; + qemu_irq output[DW_APB_GPIO_NR_PINS]; + uint32_t input; +} DWAPBGPIOState; + +#endif /* HW_GPIO_DW_APB_GPIO_H */ From fc1d62047cc3453ae7934554d7a8fd316adbfa79 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 17:48:43 +0800 Subject: [PATCH 09/14] hw/misc: add AX650X Ethernet support blocks Signed-off-by: Chao Liu --- hw/misc/Kconfig | 6 + hw/misc/ax650x-dwmac-glue.c | 165 ++++++++++++++++++++++++++++ hw/misc/ax650x-hwspinlock.c | 109 ++++++++++++++++++ hw/misc/meson.build | 4 + include/hw/misc/ax650x-dwmac-glue.h | 36 ++++++ include/hw/misc/ax650x-hwspinlock.h | 25 +++++ 6 files changed, 345 insertions(+) create mode 100644 hw/misc/ax650x-dwmac-glue.c create mode 100644 hw/misc/ax650x-hwspinlock.c create mode 100644 include/hw/misc/ax650x-dwmac-glue.h create mode 100644 include/hw/misc/ax650x-hwspinlock.h diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig index 55db0c38ebfd..34512f6f4996 100644 --- a/hw/misc/Kconfig +++ b/hw/misc/Kconfig @@ -342,3 +342,9 @@ config SOPHGO_CV1800B_CLK bool source macio/Kconfig +# AX650X DWMAC clock/reset/interface glue +config AX650X_DWMAC_GLUE + bool + +config AX650X_HWSPINLOCK + bool diff --git a/hw/misc/ax650x-dwmac-glue.c b/hw/misc/ax650x-dwmac-glue.c new file mode 100644 index 000000000000..8840f1eef005 --- /dev/null +++ b/hw/misc/ax650x-dwmac-glue.c @@ -0,0 +1,165 @@ +/* + * Axera AX650X DWMAC clock/reset/interface glue + * + * The register locations and bit assignments are extracted from the vendor + * Linux 5.15.73 dwmac-axera driver. This model stores the RGMII interface, + * speed mux, and clock enable controls and implements software-reset set/clear + * aliases. It does not model clock waveforms or RGMII timing. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "hw/core/qdev-properties.h" +#include "hw/core/registerfields.h" +#include "hw/misc/ax650x-dwmac-glue.h" +#include "migration/vmstate.h" +#include "qapi/error.h" +#include "qemu/module.h" + +REG32(GLB_EMAC1_PHY_IF, 0x94) +REG32(GLB_EMAC0_PHY_IF, 0x9c) + +REG32(CLK_MUX, 0x00) +REG32(CLK_ENABLE0, 0x04) +REG32(CLK_ENABLE1, 0x08) +REG32(CLK_DIV, 0x0c) +REG32(SW_RESET0, 0x10) +REG32(SW_RESET0_SET, 0x38) +REG32(SW_RESET0_CLEAR, 0x3c) + +static void ax650x_dwmac_reset_set_postw(RegisterInfo *reg, uint64_t value) +{ + AX650XDWMACGlueState *s = AX650X_DWMAC_GLUE(reg->opaque); + uint32_t mask = BIT(s->port ? 2 : 7); + + s->clk_regs[R_SW_RESET0] |= value & mask; + s->clk_regs[R_SW_RESET0_SET] = 0; +} + +static void ax650x_dwmac_reset_clear_postw(RegisterInfo *reg, uint64_t value) +{ + AX650XDWMACGlueState *s = AX650X_DWMAC_GLUE(reg->opaque); + uint32_t mask = BIT(s->port ? 2 : 7); + + s->clk_regs[R_SW_RESET0] &= ~(value & mask); + s->clk_regs[R_SW_RESET0_CLEAR] = 0; +} + +static const RegisterAccessInfo ax650x_dwmac_glb0_regs_info[] = { + { .name = "EMAC0_PHY_IF", .addr = A_GLB_EMAC0_PHY_IF }, +}; + +static const RegisterAccessInfo ax650x_dwmac_glb1_regs_info[] = { + { .name = "EMAC1_PHY_IF", .addr = A_GLB_EMAC1_PHY_IF }, +}; + +static const RegisterAccessInfo ax650x_dwmac_clk_regs_info[] = { + { .name = "CLK_MUX", .addr = A_CLK_MUX }, + { .name = "CLK_ENABLE0", .addr = A_CLK_ENABLE0 }, + { .name = "CLK_ENABLE1", .addr = A_CLK_ENABLE1 }, + { .name = "CLK_DIV", .addr = A_CLK_DIV }, + { .name = "SW_RESET0", .addr = A_SW_RESET0, + .ro = MAKE_64BIT_MASK(0, 32), + }, + { .name = "SW_RESET0_SET", .addr = A_SW_RESET0_SET, + .post_write = ax650x_dwmac_reset_set_postw, + }, + { .name = "SW_RESET0_CLEAR", .addr = A_SW_RESET0_CLEAR, + .post_write = ax650x_dwmac_reset_clear_postw, + }, +}; + +static const MemoryRegionOps ax650x_dwmac_glue_ops = { + .read = register_read_memory, + .write = register_write_memory, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + .unaligned = false, + }, +}; + +static void ax650x_dwmac_glue_reset(DeviceState *dev) +{ + AX650XDWMACGlueState *s = AX650X_DWMAC_GLUE(dev); + + for (unsigned int i = 0; i < AX650X_DWMAC_GLB_NR_REGS; i++) { + register_reset(&s->glb_regs_info[i]); + } + for (unsigned int i = 0; i < AX650X_DWMAC_CLK_NR_REGS; i++) { + register_reset(&s->clk_regs_info[i]); + } +} + +static void ax650x_dwmac_glue_realize(DeviceState *dev, Error **errp) +{ + AX650XDWMACGlueState *s = AX650X_DWMAC_GLUE(dev); + SysBusDevice *sbd = SYS_BUS_DEVICE(dev); + const RegisterAccessInfo *glb_regs; + size_t glb_nr_regs; + + if (s->port > 1) { + error_setg(errp, "ax650x-dwmac-glue: port must be 0 or 1"); + return; + } + + if (s->port == 0) { + glb_regs = ax650x_dwmac_glb0_regs_info; + glb_nr_regs = ARRAY_SIZE(ax650x_dwmac_glb0_regs_info); + } else { + glb_regs = ax650x_dwmac_glb1_regs_info; + glb_nr_regs = ARRAY_SIZE(ax650x_dwmac_glb1_regs_info); + } + + s->glb_reg_array = register_init_block32( + dev, glb_regs, glb_nr_regs, s->glb_regs_info, s->glb_regs, + &ax650x_dwmac_glue_ops, false, AX650X_DWMAC_GLUE_MMIO_SIZE); + s->clk_reg_array = register_init_block32( + dev, ax650x_dwmac_clk_regs_info, + ARRAY_SIZE(ax650x_dwmac_clk_regs_info), s->clk_regs_info, + s->clk_regs, &ax650x_dwmac_glue_ops, false, + AX650X_DWMAC_GLUE_MMIO_SIZE); + + sysbus_init_mmio(sbd, &s->glb_reg_array->mem); + sysbus_init_mmio(sbd, &s->clk_reg_array->mem); +} + +static const VMStateDescription vmstate_ax650x_dwmac_glue = { + .name = TYPE_AX650X_DWMAC_GLUE, + .version_id = 1, + .minimum_version_id = 1, + .fields = (const VMStateField[]) { + VMSTATE_UINT32_ARRAY(glb_regs, AX650XDWMACGlueState, + AX650X_DWMAC_GLB_NR_REGS), + VMSTATE_UINT32_ARRAY(clk_regs, AX650XDWMACGlueState, + AX650X_DWMAC_CLK_NR_REGS), + VMSTATE_END_OF_LIST(), + }, +}; + +static const Property ax650x_dwmac_glue_properties[] = { + DEFINE_PROP_UINT8("port", AX650XDWMACGlueState, port, 0), +}; + +static void ax650x_dwmac_glue_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + dc->desc = "AX650X DWMAC clock/reset/interface glue"; + dc->realize = ax650x_dwmac_glue_realize; + device_class_set_legacy_reset(dc, ax650x_dwmac_glue_reset); + dc->vmsd = &vmstate_ax650x_dwmac_glue; + device_class_set_props(dc, ax650x_dwmac_glue_properties); +} + +static const TypeInfo ax650x_dwmac_glue_types[] = { + { + .name = TYPE_AX650X_DWMAC_GLUE, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(AX650XDWMACGlueState), + .class_init = ax650x_dwmac_glue_class_init, + }, +}; +DEFINE_TYPES(ax650x_dwmac_glue_types) diff --git a/hw/misc/ax650x-hwspinlock.c b/hw/misc/ax650x-hwspinlock.c new file mode 100644 index 000000000000..e6c92d928ddb --- /dev/null +++ b/hw/misc/ax650x-hwspinlock.c @@ -0,0 +1,109 @@ +/* + * AXERA AX650X hardware spinlock + * + * The AXERA Linux driver exposes 32 locks. Each lock has an acquire register + * at lock_id * 8 and an unlock register at lock_id * 8 + 4. Reading an + * unlocked acquire register claims the lock for CPU master ID 0 and returns + * that ID (zero); a later read reports the lock busy. The vendor driver + * converts the zero owner ID into a successful hwspinlock trylock result. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "hw/misc/ax650x-hwspinlock.h" +#include "migration/vmstate.h" +#include "qemu/bitops.h" +#include "qemu/module.h" + +#define AX650X_HWSPINLOCK_STRIDE 8 + +static uint64_t ax650x_hwspinlock_read(void *opaque, hwaddr offset, + unsigned int size) +{ + AX650XHWSpinlockState *s = AX650X_HWSPINLOCK(opaque); + unsigned int lock = offset / AX650X_HWSPINLOCK_STRIDE; + + if (lock >= AX650X_HWSPINLOCK_COUNT || + offset % AX650X_HWSPINLOCK_STRIDE != 0) { + return 0; + } + + if (s->locked & BIT(lock)) { + return 1; + } + + s->locked |= BIT(lock); + return 0; +} + +static void ax650x_hwspinlock_write(void *opaque, hwaddr offset, + uint64_t value, unsigned int size) +{ + AX650XHWSpinlockState *s = AX650X_HWSPINLOCK(opaque); + unsigned int lock = offset / AX650X_HWSPINLOCK_STRIDE; + + if (lock < AX650X_HWSPINLOCK_COUNT && + offset % AX650X_HWSPINLOCK_STRIDE == sizeof(uint32_t)) { + s->locked &= ~BIT(lock); + } +} + +static const MemoryRegionOps ax650x_hwspinlock_ops = { + .read = ax650x_hwspinlock_read, + .write = ax650x_hwspinlock_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + .unaligned = false, + }, +}; + +static void ax650x_hwspinlock_reset(DeviceState *dev) +{ + AX650XHWSpinlockState *s = AX650X_HWSPINLOCK(dev); + + s->locked = 0; +} + +static void ax650x_hwspinlock_init(Object *obj) +{ + AX650XHWSpinlockState *s = AX650X_HWSPINLOCK(obj); + SysBusDevice *sbd = SYS_BUS_DEVICE(obj); + + memory_region_init_io(&s->iomem, obj, &ax650x_hwspinlock_ops, s, + TYPE_AX650X_HWSPINLOCK, + AX650X_HWSPINLOCK_MMIO_SIZE); + sysbus_init_mmio(sbd, &s->iomem); +} + +static const VMStateDescription vmstate_ax650x_hwspinlock = { + .name = TYPE_AX650X_HWSPINLOCK, + .version_id = 1, + .minimum_version_id = 1, + .fields = (const VMStateField[]) { + VMSTATE_UINT32(locked, AX650XHWSpinlockState), + VMSTATE_END_OF_LIST(), + }, +}; + +static void ax650x_hwspinlock_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + dc->desc = "AX650X hardware spinlock"; + device_class_set_legacy_reset(dc, ax650x_hwspinlock_reset); + dc->vmsd = &vmstate_ax650x_hwspinlock; +} + +static const TypeInfo ax650x_hwspinlock_types[] = { + { + .name = TYPE_AX650X_HWSPINLOCK, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(AX650XHWSpinlockState), + .instance_init = ax650x_hwspinlock_init, + .class_init = ax650x_hwspinlock_class_init, + }, +}; +DEFINE_TYPES(ax650x_hwspinlock_types) diff --git a/hw/misc/meson.build b/hw/misc/meson.build index e92e19236194..a1953b05acd1 100644 --- a/hw/misc/meson.build +++ b/hw/misc/meson.build @@ -196,3 +196,7 @@ system_ss.add(when: 'CONFIG_SBSA_REF', if_true: files('sbsa_ec.c')) # HPPA devices system_ss.add(when: 'CONFIG_LASI', if_true: files('lasi.c')) +system_ss.add(when: 'CONFIG_AX650X_DWMAC_GLUE', + if_true: files('ax650x-dwmac-glue.c')) +system_ss.add(when: 'CONFIG_AX650X_HWSPINLOCK', + if_true: files('ax650x-hwspinlock.c')) diff --git a/include/hw/misc/ax650x-dwmac-glue.h b/include/hw/misc/ax650x-dwmac-glue.h new file mode 100644 index 000000000000..d76705277dc6 --- /dev/null +++ b/include/hw/misc/ax650x-dwmac-glue.h @@ -0,0 +1,36 @@ +/* + * Axera AX650X DWMAC clock/reset/interface glue + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef HW_MISC_AX650X_DWMAC_GLUE_H +#define HW_MISC_AX650X_DWMAC_GLUE_H + +#include "hw/core/register.h" +#include "hw/core/sysbus.h" + +#define TYPE_AX650X_DWMAC_GLUE "ax650x-dwmac-glue" +OBJECT_DECLARE_SIMPLE_TYPE(AX650XDWMACGlueState, AX650X_DWMAC_GLUE) + +#define AX650X_DWMAC_GLB_REG_SIZE 0xa0 +#define AX650X_DWMAC_GLB_NR_REGS \ + (AX650X_DWMAC_GLB_REG_SIZE / sizeof(uint32_t)) +#define AX650X_DWMAC_CLK_REG_SIZE 0x40 +#define AX650X_DWMAC_CLK_NR_REGS \ + (AX650X_DWMAC_CLK_REG_SIZE / sizeof(uint32_t)) +#define AX650X_DWMAC_GLUE_MMIO_SIZE 0x1000 + +typedef struct AX650XDWMACGlueState { + SysBusDevice parent; + + uint32_t glb_regs[AX650X_DWMAC_GLB_NR_REGS]; + RegisterInfo glb_regs_info[AX650X_DWMAC_GLB_NR_REGS]; + RegisterInfoArray *glb_reg_array; + uint32_t clk_regs[AX650X_DWMAC_CLK_NR_REGS]; + RegisterInfo clk_regs_info[AX650X_DWMAC_CLK_NR_REGS]; + RegisterInfoArray *clk_reg_array; + uint8_t port; +} AX650XDWMACGlueState; + +#endif /* HW_MISC_AX650X_DWMAC_GLUE_H */ diff --git a/include/hw/misc/ax650x-hwspinlock.h b/include/hw/misc/ax650x-hwspinlock.h new file mode 100644 index 000000000000..1224091d06d3 --- /dev/null +++ b/include/hw/misc/ax650x-hwspinlock.h @@ -0,0 +1,25 @@ +/* + * AXERA AX650X hardware spinlock + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef HW_MISC_AX650X_HWSPINLOCK_H +#define HW_MISC_AX650X_HWSPINLOCK_H + +#include "hw/core/sysbus.h" + +#define TYPE_AX650X_HWSPINLOCK "ax650x-hwspinlock" +OBJECT_DECLARE_SIMPLE_TYPE(AX650XHWSpinlockState, AX650X_HWSPINLOCK) + +#define AX650X_HWSPINLOCK_MMIO_SIZE 0x1000 +#define AX650X_HWSPINLOCK_COUNT 32 + +typedef struct AX650XHWSpinlockState { + SysBusDevice parent; + + MemoryRegion iomem; + uint32_t locked; +} AX650XHWSpinlockState; + +#endif /* HW_MISC_AX650X_HWSPINLOCK_H */ From 711fb8e5b07ce5b97883371db5ad934780f15315 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 17:49:27 +0800 Subject: [PATCH 10/14] hw/net: refine DWMAC4 DMA and PHY behavior Signed-off-by: Chao Liu --- hw/net/dwmac4.c | 173 +++++++++++++++++++++++++++++----------- include/hw/net/dwmac4.h | 28 +++++-- 2 files changed, 147 insertions(+), 54 deletions(-) diff --git a/hw/net/dwmac4.c b/hw/net/dwmac4.c index 7ad434a3adde..0f1be0214d0a 100644 --- a/hw/net/dwmac4.c +++ b/hw/net/dwmac4.c @@ -8,7 +8,7 @@ * (dwmac4.h / dwmac4_dma.h / dwmac4_descs.h in the Linux stmmac driver): * * - MAC register bank 0x000..0x3ff (MAC_CONFIG, PACKET_FILTER, ADDR slots, - * MDIO, HW_FEATURE0..3, MAC_VERSION @ 0x110 = SNPSVER 0x51). + * MDIO, HW_FEATURE0..3, and board-selectable MAC_VERSION @ 0x110. * - DMA register bank 0x1000..0x11ff (DMA_BUS_MODE, per-channel block at * 0x1100 + chan*0x80: TX/RX_CONTROL, TX/RX_BASE_ADDR, RING_LEN, * INTR_ENA, CHAN_STATUS as W1C with 4.10-layout NIS/AIS). @@ -30,6 +30,7 @@ #include "net/checksum.h" #include "net/eth.h" #include "net/net.h" +#include "qapi/error.h" #include "qemu/bitops.h" #include "qemu/log.h" #include "qemu/module.h" @@ -98,6 +99,7 @@ REG32(DMA_CHAN_TX_CONTROL, 0x04) FIELD(DMA_CHAN_TX_CONTROL, ST, 0, 1) REG32(DMA_CHAN_RX_CONTROL, 0x08) FIELD(DMA_CHAN_RX_CONTROL, SR, 0, 1) + FIELD(DMA_CHAN_RX_CONTROL, RBSZ, 1, 14) REG32(DMA_CHAN_TX_BASE_ADDR_HI, 0x10) REG32(DMA_CHAN_TX_BASE_ADDR, 0x14) REG32(DMA_CHAN_RX_BASE_ADDR_HI, 0x18) @@ -168,6 +170,7 @@ REG32(DMA_CHAN_STATUS, 0x60) #define DESC_OWN BIT(31) #define TX_DESC3_FIRST BIT(29) #define TX_DESC3_LAST BIT(28) +#define TX_DESC3_CIC_MASK 0x00030000u #define TX_DESC2_IOC BIT(31) #define TX_DESC2_B1SZ_MASK 0x00003fffu /* BUFFER1_SIZE[13:0] */ #define RX_DESC3_FIRST BIT(29) @@ -258,23 +261,26 @@ static void dwmac4_update_irq(DWMAC4State *s) static void dwmac4_try_send(DWMAC4State *s, int chan) { + g_autoptr(GByteArray) frame = NULL; + bool frame_checksum = false; hwaddr base_addr = ((uint64_t)s->dma_regs[dwmac4_chan_reg(chan, A_DMA_CHAN_TX_BASE_ADDR_HI) / 4] << 32) | s->dma_regs[dwmac4_chan_reg(chan, A_DMA_CHAN_TX_BASE_ADDR) / 4]; uint32_t ring_len = s->dma_regs[dwmac4_chan_reg(chan, A_DMA_CHAN_TX_RING_LEN) / 4]; - if (!base_addr || !ring_len) { + uint64_t ring_entries = (uint64_t)ring_len + 1; + if (!base_addr) { return; } NetClientState *nc = qemu_get_queue(s->nic); - uint32_t cur = s->tx_desc_cur[chan]; + hwaddr cur = s->tx_desc_cur[chan]; if (!cur) { cur = base_addr; } - for (uint32_t i = 0; i < ring_len; i++) { + for (uint64_t i = 0; i < ring_entries; i++) { uint32_t d[4]; if (dwmac4_read_desc(cur, d)) { return; @@ -282,12 +288,13 @@ static void dwmac4_try_send(DWMAC4State *s, int chan) /* OWN=1 -> MAC owns, can TX. Otherwise ring drained for now. */ if (!(d[3] & DESC_OWN)) { - return; + break; } bool first = d[3] & TX_DESC3_FIRST; bool last = d[3] & TX_DESC3_LAST; bool ioc = d[2] & TX_DESC2_IOC; + bool checksum_insertion = d[3] & TX_DESC3_CIC_MASK; uint32_t b1sz = d[2] & TX_DESC2_B1SZ_MASK; /* * dwmac4 TDES0 holds buffer-1 address (low 32). In extended 64-bit @@ -299,24 +306,39 @@ static void dwmac4_try_send(DWMAC4State *s, int chan) */ hwaddr b1addr = ((uint64_t)d[1] << 32) | d[0]; - if (first && last) { - /* Common case: single-buffer frame in one descriptor. */ - g_autofree uint8_t *buf = NULL; - if (b1sz && b1sz <= 65536) { - buf = g_malloc(b1sz); - if (dma_memory_read(&address_space_memory, b1addr, buf, b1sz, - MEMTXATTRS_UNSPECIFIED)) { - qemu_log_mask(LOG_GUEST_ERROR, - "dwmac4: TX buffer read failed @ 0x%" - HWADDR_PRIx "\n", b1addr); - g_free(buf); - return; - } - net_checksum_calculate(buf, b1sz, 0); - qemu_send_packet(nc, buf, b1sz); + if (first) { + g_clear_pointer(&frame, g_byte_array_unref); + frame = g_byte_array_new(); + frame_checksum = checksum_insertion; + } + + if (frame && b1sz) { + size_t offset = frame->len; + + if (offset + b1sz > 65536) { + qemu_log_mask(LOG_GUEST_ERROR, + "dwmac4: TX frame exceeds 64 KiB\n"); + return; + } + g_byte_array_set_size(frame, offset + b1sz); + if (dma_memory_read(&address_space_memory, b1addr, + frame->data + offset, b1sz, + MEMTXATTRS_UNSPECIFIED)) { + qemu_log_mask(LOG_GUEST_ERROR, + "dwmac4: TX buffer read failed @ 0x%" + HWADDR_PRIx "\n", b1addr); + return; } } + if (last && frame) { + if (frame_checksum) { + net_checksum_calculate(frame->data, frame->len, CSUM_ALL); + } + qemu_send_packet(nc, frame->data, frame->len); + g_clear_pointer(&frame, g_byte_array_unref); + } + /* Write-back: clear OWN. Error summary (bit15) left clear. */ d[3] &= ~DESC_OWN; if (dwmac4_write_desc(cur, d)) { @@ -324,7 +346,7 @@ static void dwmac4_try_send(DWMAC4State *s, int chan) } cur += 16; - if (cur >= base_addr + (uint64_t)ring_len * 16) { + if (cur >= base_addr + ring_entries * 16) { cur = base_addr; } @@ -335,10 +357,6 @@ static void dwmac4_try_send(DWMAC4State *s, int chan) dwmac4_update_irq(s); } - if (last) { - /* Stop after a complete frame so the guest can observe IRQ. */ - break; - } } s->tx_desc_cur[chan] = cur; @@ -379,17 +397,18 @@ static ssize_t dwmac4_receive(NetClientState *nc, const uint8_t *buf, size_t len A_DMA_CHAN_RX_BASE_ADDR) / 4]; uint32_t ring_len = s->dma_regs[dwmac4_chan_reg(chan, A_DMA_CHAN_RX_RING_LEN) / 4]; - if (!base_addr || !ring_len) { + uint64_t ring_entries = (uint64_t)ring_len + 1; + if (!base_addr) { return len; /* no ring posted yet - silently drop */ } - uint32_t cur = s->rx_desc_cur[chan]; + hwaddr cur = s->rx_desc_cur[chan]; if (!cur) { cur = base_addr; } /* Find the next descriptor the MAC owns. */ - for (uint32_t i = 0; i < ring_len; i++) { + for (uint64_t i = 0; i < ring_entries; i++) { uint32_t d[4]; if (dwmac4_read_desc(cur, d)) { return len; @@ -400,8 +419,10 @@ static ssize_t dwmac4_receive(NetClientState *nc, const uint8_t *buf, size_t len } hwaddr b1addr = ((uint64_t)d[1] << 32) | d[0]; - /* RDES2[14:0] is BUFFER1_SIZE; we cap the write to that. */ - uint32_t b1sz = d[2] & 0x3fff; + /* DWMAC4 keeps the receive buffer size in DMA_CHAN_RX_CONTROL. */ + uint32_t rxctl = s->dma_regs[dwmac4_chan_reg( + chan, A_DMA_CHAN_RX_CONTROL) / 4]; + uint32_t b1sz = FIELD_EX32(rxctl, DMA_CHAN_RX_CONTROL, RBSZ); uint32_t to_write = MIN((uint32_t)len, b1sz); if (to_write && dma_memory_write(&address_space_memory, b1addr, buf, @@ -418,14 +439,20 @@ static ssize_t dwmac4_receive(NetClientState *nc, const uint8_t *buf, size_t len */ d[3] &= ~DESC_OWN; d[3] &= ~RX_DESC3_PKT_SIZE_MASK; - d[3] |= RX_DESC3_FIRST | RX_DESC3_LAST | ((uint32_t)len & - RX_DESC3_PKT_SIZE_MASK); + /* + * GMAC4 reports a packet size that includes the four-byte Ethernet + * FCS, although the DMA buffer does not contain the FCS. stmmac + * accounts for that contract by subtracting ETH_FCS_LEN before it + * hands the skb to the network stack. + */ + d[3] |= RX_DESC3_FIRST | RX_DESC3_LAST | + (((uint32_t)len + ETH_FCS_LEN) & RX_DESC3_PKT_SIZE_MASK); if (dwmac4_write_desc(cur, d)) { return len; } cur += 16; - if (cur >= base_addr + (uint64_t)ring_len * 16) { + if (cur >= base_addr + ring_entries * 16) { cur = base_addr; } s->rx_desc_cur[chan] = cur; @@ -485,11 +512,7 @@ static void dwmac4_mdio(DWMAC4State *s, uint32_t v) s->mac_regs[R_GMAC_MDIO_ADDR] = v; return; } - if (pa > 1) { - /* - * The ROC-RK3588S-PC DT uses PHY address 1. Keep address 0 alive - * for the simple QEMU DT and report all other addresses as idle. - */ + if (pa != s->phy_addr) { if (!write) { s->mac_regs[R_GMAC_MDIO_DATA] = 0xffff; } @@ -499,9 +522,20 @@ static void dwmac4_mdio(DWMAC4State *s, uint32_t v) if (write) { data = s->mac_regs[R_GMAC_MDIO_DATA] & 0xffff; - s->phy_regs[gr] = data; + if (gr == 31) { + s->phy_page = data; + } else if (s->phy_page == 0) { + /* BMCR reset and autoneg restart are self-clearing commands. */ + s->phy_regs[gr] = gr == 0 ? data & ~(BIT(15) | BIT(9)) : data; + } } else { - s->mac_regs[R_GMAC_MDIO_DATA] = s->phy_regs[gr]; + if (gr == 31) { + s->mac_regs[R_GMAC_MDIO_DATA] = s->phy_page; + } else { + /* Vendor pages are accepted as RAZ/WI; page 0 is clause-22. */ + s->mac_regs[R_GMAC_MDIO_DATA] = + s->phy_page == 0 ? s->phy_regs[gr] : 0; + } } /* BUSY is self-clearing in hardware; model that. */ @@ -698,7 +732,7 @@ static const RegisterAccessInfo mac_regs_info[] = { .ro = MAKE_64BIT_MASK(0, 32), }, { .name = "GMAC4_VERSION", .addr = A_GMAC4_VERSION, - .reset = DWMAC4_VERSION_RESET, + .reset = DWMAC4_DEFAULT_SNPS_VERSION, .ro = MAKE_64BIT_MASK(0, 32), }, { .name = "GMAC_DEBUG", .addr = A_GMAC_DEBUG, @@ -710,7 +744,8 @@ static const RegisterAccessInfo mac_regs_info[] = { * dwmac4.h: RXCOESEL=16, TXCOSEL=14, TSSEL=12, MMCSEL=8, MGKSEL=7. */ { .name = "GMAC_HW_FEATURE0", .addr = A_GMAC_HW_FEATURE0, - .reset = BIT(16) | BIT(14) | BIT(12) | BIT(8) | BIT(7), + .reset = BIT(16) | BIT(14) | BIT(12) | BIT(8) | BIT(7) | BIT(6) | + BIT(5) | BIT(1) | BIT(0), .ro = MAKE_64BIT_MASK(0, 32), }, { .name = "GMAC_HW_FEATURE1", .addr = A_GMAC_HW_FEATURE1, @@ -954,6 +989,17 @@ static void dwmac4_reset(DeviceState *dev) s->rx_desc_cur[chan] = 0; } memcpy(s->phy_regs, phy_reg_init, sizeof(s->phy_regs)); + s->phy_page = 0; + s->phy_regs[2] = s->phy_id1; + s->phy_regs[3] = s->phy_id2; + + s->mac_regs[R_GMAC4_VERSION] = + ((uint32_t)s->user_version << 8) | s->snps_version; + s->mac_regs[R_GMAC_HW_FEATURE1] = + ((s->dma_width == 40 ? 1 : s->dma_width == 48 ? 2 : 0) << 14) | + ((ctz32(s->tx_fifo_size) - 7) << 6) | + (ctz32(s->rx_fifo_size) - 7) | + (s->tso ? BIT(18) : 0); /* Reflect the configured MAC address into MAC_ADDR_HIGH0/LOW0. */ s->mac_regs[R_GMAC_ADDR_HIGH0] = 0x80000000 | @@ -970,6 +1016,27 @@ static void dwmac4_realize(DeviceState *dev, Error **errp) DWMAC4State *s = DWMAC4(dev); SysBusDevice *sbd = SYS_BUS_DEVICE(dev); + if (s->dma_width != 32 && s->dma_width != 40 && s->dma_width != 48) { + error_setg(errp, "dwmac4: dma-width must be 32, 40, or 48"); + return; + } + if (!is_power_of_2(s->tx_fifo_size) || s->tx_fifo_size < 128 || + s->tx_fifo_size > 4 * MiB) { + error_setg(errp, "dwmac4: tx-fifo-size must be a power of two " + "between 128 bytes and 4 MiB"); + return; + } + if (!is_power_of_2(s->rx_fifo_size) || s->rx_fifo_size < 128 || + s->rx_fifo_size > 4 * MiB) { + error_setg(errp, "dwmac4: rx-fifo-size must be a power of two " + "between 128 bytes and 4 MiB"); + return; + } + if (s->phy_addr > 31) { + error_setg(errp, "dwmac4: phy-addr must be in the range 0..31"); + return; + } + /* * Two register blocks, each giving us a MemoryRegion + RegisterInfo * array. We keep references to the arrays so our top-level dispatcher @@ -1006,20 +1073,34 @@ static void dwmac4_unrealize(DeviceState *dev) static const VMStateDescription vmstate_dwmac4 = { .name = TYPE_DWMAC4, - .version_id = 1, - .minimum_version_id = 1, + .version_id = 2, + .minimum_version_id = 2, .fields = (const VMStateField[]) { VMSTATE_UINT32_ARRAY(mac_regs, DWMAC4State, DWMAC4_MAC_NR_REGS), VMSTATE_UINT32_ARRAY(dma_regs, DWMAC4State, DWMAC4_DMA_NR_REGS), - VMSTATE_UINT32_ARRAY(tx_desc_cur, DWMAC4State, DWMAC4_NR_CHANNELS), - VMSTATE_UINT32_ARRAY(rx_desc_cur, DWMAC4State, DWMAC4_NR_CHANNELS), + VMSTATE_UINT64_ARRAY(tx_desc_cur, DWMAC4State, DWMAC4_NR_CHANNELS), + VMSTATE_UINT64_ARRAY(rx_desc_cur, DWMAC4State, DWMAC4_NR_CHANNELS), VMSTATE_UINT16_ARRAY(phy_regs, DWMAC4State, 32), + VMSTATE_UINT16(phy_page, DWMAC4State), VMSTATE_END_OF_LIST(), }, }; static const Property dwmac4_properties[] = { DEFINE_NIC_PROPERTIES(DWMAC4State, conf), + DEFINE_PROP_UINT8("snps-version", DWMAC4State, snps_version, + DWMAC4_DEFAULT_SNPS_VERSION), + DEFINE_PROP_UINT8("user-version", DWMAC4State, user_version, 0), + DEFINE_PROP_UINT8("dma-width", DWMAC4State, dma_width, + DWMAC4_DEFAULT_DMA_WIDTH), + DEFINE_PROP_UINT8("phy-addr", DWMAC4State, phy_addr, 1), + DEFINE_PROP_UINT16("phy-id1", DWMAC4State, phy_id1, 0x001c), + DEFINE_PROP_UINT16("phy-id2", DWMAC4State, phy_id2, 0xc916), + DEFINE_PROP_UINT32("tx-fifo-size", DWMAC4State, tx_fifo_size, + DWMAC4_DEFAULT_FIFO_SIZE), + DEFINE_PROP_UINT32("rx-fifo-size", DWMAC4State, rx_fifo_size, + DWMAC4_DEFAULT_FIFO_SIZE), + DEFINE_PROP_BOOL("tso", DWMAC4State, tso, false), }; static void dwmac4_class_init(ObjectClass *klass, const void *data) diff --git a/include/hw/net/dwmac4.h b/include/hw/net/dwmac4.h index 63fe4b477eb2..817dbfe83b6b 100644 --- a/include/hw/net/dwmac4.h +++ b/include/hw/net/dwmac4.h @@ -21,13 +21,13 @@ #include "net/net.h" /* - * MAC_VERSION (GMAC4_VERSION @ 0x110) synth-id. Linux stmmac reads SNPSVER - * (bits[7:0]) and routes through stmmac_hw[]: min_id=DWMAC_CORE_5_10 (0x51) - * selects dwmac510_ops / dwmac410_dma_ops / dwmac4_desc_ops. Real RK3588 - * silicon reports 0x51; the contract pins the model to 0x51. + * MAC_VERSION (GMAC4_VERSION @ 0x110) contains the Synopsys version in + * bits[7:0] and a user version in bits[15:8]. Boards select their reported + * version through properties; the defaults retain the original 0x51 model. */ -#define DWMAC4_SNPSVER 0x51u -#define DWMAC4_VERSION_RESET (DWMAC4_SNPSVER & 0xff) +#define DWMAC4_DEFAULT_SNPS_VERSION 0x51u +#define DWMAC4_DEFAULT_DMA_WIDTH 32u +#define DWMAC4_DEFAULT_FIFO_SIZE 4096u /* MAC register bank window. Covers 0x000..0x3ff (some slots beyond 0x300). */ #define DWMAC4_MAC_REG_SIZE 0x400 @@ -66,11 +66,23 @@ typedef struct DWMAC4State { RegisterInfoArray *dma_reg_array; /* Per-channel descriptor-ring cursor state (not part of the reg bank). */ - uint32_t tx_desc_cur[DWMAC4_NR_CHANNELS]; - uint32_t rx_desc_cur[DWMAC4_NR_CHANNELS]; + uint64_t tx_desc_cur[DWMAC4_NR_CHANNELS]; + uint64_t rx_desc_cur[DWMAC4_NR_CHANNELS]; /* MDIO clause-22 PHY scratch (minimal: link up, full-duplex 1G). */ uint16_t phy_regs[32]; + uint16_t phy_page; + + /* Board-selectable synthesis and PHY identity. */ + uint8_t snps_version; + uint8_t user_version; + uint8_t dma_width; + uint8_t phy_addr; + uint16_t phy_id1; + uint16_t phy_id2; + uint32_t tx_fifo_size; + uint32_t rx_fifo_size; + bool tso; } DWMAC4State; #define TYPE_DWMAC4 "dwmac4" From 339e315c3e524cce14bc26859cb2839b53d0baa9 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 21:49:19 +0800 Subject: [PATCH 11/14] hw/arm: integrate AX650X DWMAC topology Instantiate the AX650X Ethernet topology and verify the glue, GPIO, hwspinlock, MDIO, PHY, DMA and interrupt behavior with qtest. Signed-off-by: Chao Liu --- hw/arm/Kconfig | 5 + hw/arm/ax650x-dwmac.c | 317 ++++++++++++++++++++++++++++++ hw/arm/ax650x.c | 3 + hw/arm/meson.build | 3 +- include/hw/arm/ax650x-dwmac.h | 15 ++ tests/qtest/ax650x-pyramid-test.c | 222 +++++++++++++++++++++ 6 files changed, 564 insertions(+), 1 deletion(-) create mode 100644 hw/arm/ax650x-dwmac.c create mode 100644 include/hw/arm/ax650x-dwmac.h diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig index 239ca712039e..2726fc56d08a 100644 --- a/hw/arm/Kconfig +++ b/hw/arm/Kconfig @@ -42,9 +42,14 @@ config AX650X_PYRAMID default y depends on TCG && AARCH64 select ARM_GIC + select AX650X_DWMAC_GLUE + select AX650X_HWSPINLOCK select AX650X_SDHCI select DEVICE_TREE + select DW_APB_GPIO + select DWMAC4 select SERIAL_MM + select UNIMP config CUBIEBOARD bool diff --git a/hw/arm/ax650x-dwmac.c b/hw/arm/ax650x-dwmac.c new file mode 100644 index 000000000000..0fa672758fc0 --- /dev/null +++ b/hw/arm/ax650x-dwmac.c @@ -0,0 +1,317 @@ +/* + * AX650X DWMAC board integration + * + * This file owns the networking-related board topology and FDT nodes. The + * reusable devices themselves live in hw/net, hw/gpio, and hw/misc. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "hw/arm/ax650x-dwmac.h" +#include "hw/arm/fdt.h" +#include "hw/core/qdev-properties.h" +#include "hw/core/sysbus.h" +#include "hw/gpio/dw-apb-gpio.h" +#include "hw/misc/ax650x-dwmac-glue.h" +#include "hw/misc/ax650x-hwspinlock.h" +#include "hw/misc/unimp.h" +#include "hw/net/dwmac4.h" +#include "net/net.h" +#include "system/device_tree.h" + +#define AX650X_DWMAC_NR 2 +#define AX650X_DWMAC0_BASE 0x10140000 +#define AX650X_DWMAC1_BASE 0x30800000 +#define AX650X_DWMAC0_IRQ 104 +#define AX650X_DWMAC1_IRQ 186 +#define AX650X_DWMAC0_GLB_BASE 0x10000000 +#define AX650X_DWMAC1_GLB_BASE 0x30000000 +#define AX650X_DWMAC0_CLK_BASE 0x10010000 +#define AX650X_DWMAC1_CLK_BASE 0x30010000 +#define AX650X_GPIO0_BASE 0x02003000 +#define AX650X_GPIO1_BASE 0x02004000 +#define AX650X_HWSPINLOCK_BASE 0x04510000 +#define AX650X_DPHY_BASE 0x13c00000 +#define AX650X_DPHY_SIZE 0x00080000 + +#define AX650X_DWMAC0_CLOCK_HZ 400000000 +#define AX650X_DWMAC1_CLOCK_HZ 500000000 +#define AX650X_EPHY_CLOCK_HZ 25000000 +#define AX650X_RGMII_CLOCK_HZ 125000000 +#define AX650X_RMII_CLOCK_HZ 50000000 +#define AX650X_PTP_CLOCK_HZ 50000000 +#define AX650X_HWSPINLOCK_CLOCK_HZ 200000000 + +static const hwaddr dwmac_base[AX650X_DWMAC_NR] = { + AX650X_DWMAC0_BASE, AX650X_DWMAC1_BASE, +}; + +static const unsigned int dwmac_irq[AX650X_DWMAC_NR] = { + AX650X_DWMAC0_IRQ, AX650X_DWMAC1_IRQ, +}; + +static const hwaddr dwmac_glb_base[AX650X_DWMAC_NR] = { + AX650X_DWMAC0_GLB_BASE, AX650X_DWMAC1_GLB_BASE, +}; + +static const hwaddr dwmac_clk_base[AX650X_DWMAC_NR] = { + AX650X_DWMAC0_CLK_BASE, AX650X_DWMAC1_CLK_BASE, +}; + +static const hwaddr gpio_base[AX650X_DWMAC_NR] = { + AX650X_GPIO0_BASE, AX650X_GPIO1_BASE, +}; + +static const char *const eth_path[AX650X_DWMAC_NR] = { + "/soc/ethernet@10140000", + "/soc/ethernet@30800000", +}; + +static const char *const gpio_path[AX650X_DWMAC_NR] = { + "/soc/gpio@2003000", + "/soc/gpio@2004000", +}; + +void ax650x_dwmac_create(DeviceState *gic) +{ + DeviceState *hwspinlock = qdev_new(TYPE_AX650X_HWSPINLOCK); + SysBusDevice *sbd = SYS_BUS_DEVICE(hwspinlock); + + sysbus_realize_and_unref(sbd, &error_fatal); + sysbus_mmio_map(sbd, 0, AX650X_HWSPINLOCK_BASE); + + /* + * The vendor AXERA GPIO driver reads all eight DPHY windows before it + * registers GPIO lines. DPHY behavior is not part of networking, but a + * named unimplemented window keeps those discovery reads non-fatal. + */ + create_unimplemented_device("ax650x.dphy", AX650X_DPHY_BASE, + AX650X_DPHY_SIZE); + + for (unsigned int i = 0; i < AX650X_DWMAC_NR; i++) { + DeviceState *gpio = qdev_new(TYPE_DW_APB_GPIO); + DeviceState *glue = qdev_new(TYPE_AX650X_DWMAC_GLUE); + DeviceState *mac = qdev_new(TYPE_DWMAC4); + + sbd = SYS_BUS_DEVICE(gpio); + sysbus_realize_and_unref(sbd, &error_fatal); + sysbus_mmio_map(sbd, 0, gpio_base[i]); + + qdev_prop_set_uint8(glue, "port", i); + sbd = SYS_BUS_DEVICE(glue); + sysbus_realize_and_unref(sbd, &error_fatal); + sysbus_mmio_map(sbd, 0, dwmac_glb_base[i]); + sysbus_mmio_map(sbd, 1, dwmac_clk_base[i]); + + qemu_configure_nic_device(mac, i == 0, i == 0 ? "gmac0" : "gmac1"); + qdev_prop_set_uint8(mac, "snps-version", 0x52); + qdev_prop_set_uint8(mac, "user-version", 0x10); + qdev_prop_set_uint8(mac, "dma-width", 40); + qdev_prop_set_uint8(mac, "phy-addr", 1); + qdev_prop_set_uint16(mac, "phy-id1", 0x937c); + qdev_prop_set_uint16(mac, "phy-id2", 0x4030); + qdev_prop_set_uint32(mac, "rx-fifo-size", 65536); + qdev_prop_set_uint32(mac, "tx-fifo-size", 32768); + sbd = SYS_BUS_DEVICE(mac); + sysbus_realize_and_unref(sbd, &error_fatal); + sysbus_mmio_map(sbd, 0, dwmac_base[i]); + sysbus_connect_irq(sbd, 0, qdev_get_gpio_in(gic, dwmac_irq[i])); + } +} + +static uint32_t ax650x_fdt_add_fixed_clock(void *fdt, const char *path, + uint32_t frequency) +{ + uint32_t phandle; + + qemu_fdt_add_subnode(fdt, path); + qemu_fdt_setprop_string(fdt, path, "compatible", "fixed-clock"); + qemu_fdt_setprop_cell(fdt, path, "#clock-cells", 0); + qemu_fdt_setprop_cell(fdt, path, "clock-frequency", frequency); + phandle = qemu_fdt_alloc_phandle(fdt); + qemu_fdt_setprop_cell(fdt, path, "phandle", phandle); + + return phandle; +} + +static void ax650x_dwmac_add_phy_fdt(void *fdt, const char *path, + uint32_t *phandle) +{ + qemu_fdt_add_subnode(fdt, path); + qemu_fdt_setprop_string(fdt, path, "compatible", + "ethernet-phy-id937c.4030"); + qemu_fdt_setprop_cell(fdt, path, "reg", 1); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,led-enable", 0x5); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,led-mode", 0x6251); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,led-period", 0x3); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,led-on", 0x2); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,led-polarity", 0x1c00); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,patch-enable", 0x1); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,rgmii-enable", 0x5); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,clk-enable", 0x9); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,fld-enable", 0x3); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,fld-delay", 0); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,wol-enable", 0x3); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,interrupt-enable", 0x5); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,downshift-enable", 0x3); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,downshift-count", 0x3); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,work_mode-enable", 0x1); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,work_mode-mode", 0); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,lpbk-enable", 0x1); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,lpbk-mode", 0x2); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,slew_rate-enable", 0x1); + qemu_fdt_setprop_cell(fdt, path, "jl2xxx,rxc_out-enable", 0x1); + *phandle = qemu_fdt_alloc_phandle(fdt); + qemu_fdt_setprop_cell(fdt, path, "phandle", *phandle); +} + +void ax650x_dwmac_create_fdt(void *fdt) +{ + static const unsigned int phy_reset_pin[AX650X_DWMAC_NR] = { 7, 11 }; + const char clock_names[] = + "emac_aclk\0ephy_clk\0rgmii_tx_clk\0rmii_phy_clk\0rmii_rx_clk\0" + "ptp_clk"; + uint32_t dwmac_clock_phandles[AX650X_DWMAC_NR]; + uint32_t gpio_phandles[AX650X_DWMAC_NR]; + uint32_t ephy_clock_phandle; + uint32_t rgmii_clock_phandle; + uint32_t rmii_clock_phandle; + uint32_t ptp_clock_phandle; + uint32_t hwspinlock_clock_phandle; + uint32_t stmmac_axi_phandle; + uint32_t mtl_rx_phandle; + uint32_t mtl_tx_phandle; + + qemu_fdt_setprop_string(fdt, "/aliases", "ethernet0", eth_path[0]); + qemu_fdt_setprop_string(fdt, "/aliases", "ethernet1", eth_path[1]); + + dwmac_clock_phandles[0] = ax650x_fdt_add_fixed_clock( + fdt, "/dwmac0-clock", AX650X_DWMAC0_CLOCK_HZ); + dwmac_clock_phandles[1] = ax650x_fdt_add_fixed_clock( + fdt, "/dwmac1-clock", AX650X_DWMAC1_CLOCK_HZ); + ephy_clock_phandle = ax650x_fdt_add_fixed_clock( + fdt, "/ephy-clock", AX650X_EPHY_CLOCK_HZ); + rgmii_clock_phandle = ax650x_fdt_add_fixed_clock( + fdt, "/rgmii-clock", AX650X_RGMII_CLOCK_HZ); + rmii_clock_phandle = ax650x_fdt_add_fixed_clock( + fdt, "/rmii-clock", AX650X_RMII_CLOCK_HZ); + ptp_clock_phandle = ax650x_fdt_add_fixed_clock( + fdt, "/ptp-clock", AX650X_PTP_CLOCK_HZ); + hwspinlock_clock_phandle = ax650x_fdt_add_fixed_clock( + fdt, "/hwspinlock-clock", AX650X_HWSPINLOCK_CLOCK_HZ); + + qemu_fdt_add_subnode(fdt, "/soc/ax_hwspinlock@4510000"); + qemu_fdt_setprop_string(fdt, "/soc/ax_hwspinlock@4510000", "compatible", + "axera,hwspinlock-r1p0"); + qemu_fdt_setprop_sized_cells(fdt, "/soc/ax_hwspinlock@4510000", "reg", + 2, AX650X_HWSPINLOCK_BASE, + 2, AX650X_HWSPINLOCK_MMIO_SIZE); + qemu_fdt_setprop_cell(fdt, "/soc/ax_hwspinlock@4510000", "clocks", + hwspinlock_clock_phandle); + qemu_fdt_setprop_string(fdt, "/soc/ax_hwspinlock@4510000", "clock-names", + "spinlock_pclk"); + qemu_fdt_setprop_string(fdt, "/soc/ax_hwspinlock@4510000", "status", + "okay"); + + qemu_fdt_add_subnode(fdt, "/soc/stmmac-axi-config"); + qemu_fdt_setprop_cells(fdt, "/soc/stmmac-axi-config", + "snps,wr_osr_lmt", 15); + qemu_fdt_setprop_cells(fdt, "/soc/stmmac-axi-config", + "snps,rd_osr_lmt", 15); + qemu_fdt_setprop_cells(fdt, "/soc/stmmac-axi-config", "snps,blen", + 0, 0, 0, 32, 16, 8, 4); + stmmac_axi_phandle = qemu_fdt_alloc_phandle(fdt); + qemu_fdt_setprop_cell(fdt, "/soc/stmmac-axi-config", "phandle", + stmmac_axi_phandle); + + qemu_fdt_add_subnode(fdt, "/soc/rx-queues-config"); + qemu_fdt_setprop_cell(fdt, "/soc/rx-queues-config", + "snps,rx-queues-to-use", 1); + qemu_fdt_add_subnode(fdt, "/soc/rx-queues-config/queue0"); + mtl_rx_phandle = qemu_fdt_alloc_phandle(fdt); + qemu_fdt_setprop_cell(fdt, "/soc/rx-queues-config", "phandle", + mtl_rx_phandle); + + qemu_fdt_add_subnode(fdt, "/soc/tx-queues-config"); + qemu_fdt_setprop_cell(fdt, "/soc/tx-queues-config", + "snps,tx-queues-to-use", 1); + qemu_fdt_add_subnode(fdt, "/soc/tx-queues-config/queue0"); + mtl_tx_phandle = qemu_fdt_alloc_phandle(fdt); + qemu_fdt_setprop_cell(fdt, "/soc/tx-queues-config", "phandle", + mtl_tx_phandle); + + for (unsigned int i = 0; i < AX650X_DWMAC_NR; i++) { + g_autofree char *gpio_port_path = + g_strdup_printf("%s/gpio-controller@0", gpio_path[i]); + g_autofree char *mdio_path = g_strdup_printf("%s/mdio", eth_path[i]); + g_autofree char *phy_path = g_strdup_printf("%s/jl2xxx-phy@1", + mdio_path); + uint32_t phy_phandle; + + qemu_fdt_add_subnode(fdt, gpio_path[i]); + qemu_fdt_setprop_string(fdt, gpio_path[i], "compatible", + "snps,ax-apb-gpio"); + qemu_fdt_setprop_cell(fdt, gpio_path[i], "#address-cells", 1); + qemu_fdt_setprop_cell(fdt, gpio_path[i], "#size-cells", 0); + qemu_fdt_setprop_sized_cells(fdt, gpio_path[i], "reg", + 2, gpio_base[i], + 2, DW_APB_GPIO_MMIO_SIZE); + if (i == 0) { + qemu_fdt_setprop_cell(fdt, gpio_path[i], "hwlock_id", 12); + } + qemu_fdt_setprop_string(fdt, gpio_path[i], "status", "okay"); + + qemu_fdt_add_subnode(fdt, gpio_port_path); + qemu_fdt_setprop_string(fdt, gpio_port_path, "compatible", + "snps,dw-apb-gpio-port"); + qemu_fdt_setprop_cell(fdt, gpio_port_path, "reg", 0); + qemu_fdt_setprop(fdt, gpio_port_path, "gpio-controller", NULL, 0); + qemu_fdt_setprop_cell(fdt, gpio_port_path, "#gpio-cells", 2); + qemu_fdt_setprop_cell(fdt, gpio_port_path, "snps,nr-gpios", + DW_APB_GPIO_NR_PINS); + gpio_phandles[i] = qemu_fdt_alloc_phandle(fdt); + qemu_fdt_setprop_cell(fdt, gpio_port_path, "phandle", + gpio_phandles[i]); + + qemu_fdt_add_subnode(fdt, eth_path[i]); + qemu_fdt_setprop_string(fdt, eth_path[i], "compatible", + "axera,dwmac-4.10a"); + qemu_fdt_setprop_sized_cells(fdt, eth_path[i], "reg", + 2, dwmac_base[i], + 2, DWMAC4_MMIO_SIZE); + qemu_fdt_setprop_cells(fdt, eth_path[i], "interrupts", + GIC_FDT_IRQ_TYPE_SPI, dwmac_irq[i], + GIC_FDT_IRQ_FLAGS_LEVEL_HI); + qemu_fdt_setprop_string(fdt, eth_path[i], "interrupt-names", + "macirq"); + qemu_fdt_setprop_cells(fdt, eth_path[i], "clocks", + dwmac_clock_phandles[i], ephy_clock_phandle, + rgmii_clock_phandle, rmii_clock_phandle, + rmii_clock_phandle, ptp_clock_phandle); + qemu_fdt_setprop(fdt, eth_path[i], "clock-names", clock_names, + sizeof(clock_names)); + qemu_fdt_setprop_cell(fdt, eth_path[i], "rx-fifo-depth", 65536); + qemu_fdt_setprop_cell(fdt, eth_path[i], "tx-fifo-depth", 32768); + qemu_fdt_setprop_cell(fdt, eth_path[i], "snps,axi-config", + stmmac_axi_phandle); + qemu_fdt_setprop_cell(fdt, eth_path[i], "snps,mtl-rx-config", + mtl_rx_phandle); + qemu_fdt_setprop_cell(fdt, eth_path[i], "snps,mtl-tx-config", + mtl_tx_phandle); + qemu_fdt_setprop_cells(fdt, eth_path[i], "phy-rst-gpio", + gpio_phandles[i], phy_reset_pin[i], 0); + qemu_fdt_setprop_string(fdt, eth_path[i], "phy-mode", "rgmii"); + qemu_fdt_setprop_string(fdt, eth_path[i], "status", "okay"); + + qemu_fdt_add_subnode(fdt, mdio_path); + qemu_fdt_setprop_string(fdt, mdio_path, "compatible", + "snps,dwmac-mdio"); + qemu_fdt_setprop_cell(fdt, mdio_path, "#address-cells", 1); + qemu_fdt_setprop_cell(fdt, mdio_path, "#size-cells", 0); + + ax650x_dwmac_add_phy_fdt(fdt, phy_path, &phy_phandle); + qemu_fdt_setprop_cell(fdt, eth_path[i], "phy-handle", phy_phandle); + } +} diff --git a/hw/arm/ax650x.c b/hw/arm/ax650x.c index 9b4732526533..d68ccfd299ba 100644 --- a/hw/arm/ax650x.c +++ b/hw/arm/ax650x.c @@ -10,6 +10,7 @@ #include "qemu/error-report.h" #include "qemu/units.h" #include "qapi/error.h" +#include "hw/arm/ax650x-dwmac.h" #include "hw/arm/boot.h" #include "hw/arm/bsa.h" #include "hw/arm/fdt.h" @@ -398,6 +399,7 @@ static void ax650x_create_fdt(AX650XPyramidState *s) qemu_fdt_setprop_cell(s->fdt, "/soc", "#address-cells", 2); qemu_fdt_setprop_cell(s->fdt, "/soc", "#size-cells", 2); qemu_fdt_setprop(s->fdt, "/soc", "ranges", NULL, 0); + ax650x_dwmac_create_fdt(s->fdt); qemu_fdt_add_subnode(s->fdt, uart_path); qemu_fdt_setprop_string(s->fdt, uart_path, "compatible", @@ -490,6 +492,7 @@ static void ax650x_pyramid_init(MachineState *machine) ax650x_create_cpus(s); ax650x_create_gic(s); ax650x_create_emmc(s); + ax650x_dwmac_create(s->gic); serial_mm_init(sysmem, AX650X_UART0_BASE, 2, qdev_get_gpio_in(s->gic, AX650X_UART0_IRQ), diff --git a/hw/arm/meson.build b/hw/arm/meson.build index 9642db354d30..2b7d24fca66a 100644 --- a/hw/arm/meson.build +++ b/hw/arm/meson.build @@ -1,6 +1,7 @@ arm_common_ss = ss.source_set() arm_common_ss.add(when: 'CONFIG_ARM_VIRT', if_true: files('virt.c')) -arm_common_ss.add(when: 'CONFIG_AX650X_PYRAMID', if_true: files('ax650x.c')) +arm_common_ss.add(when: 'CONFIG_AX650X_PYRAMID', + if_true: files('ax650x.c', 'ax650x-dwmac.c')) arm_common_ss.add(when: 'CONFIG_ACPI', if_true: files('virt-acpi-build.c')) arm_common_ss.add(when: 'CONFIG_DIGIC', if_true: files('digic_boards.c')) arm_common_ss.add(when: 'CONFIG_EMCRAFT_SF2', if_true: files('msf2-som.c')) diff --git a/include/hw/arm/ax650x-dwmac.h b/include/hw/arm/ax650x-dwmac.h new file mode 100644 index 000000000000..47fa262dc405 --- /dev/null +++ b/include/hw/arm/ax650x-dwmac.h @@ -0,0 +1,15 @@ +/* + * AX650X DWMAC board integration + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef HW_ARM_AX650X_DWMAC_H +#define HW_ARM_AX650X_DWMAC_H + +#include "hw/core/qdev.h" + +void ax650x_dwmac_create(DeviceState *gic); +void ax650x_dwmac_create_fdt(void *fdt); + +#endif /* HW_ARM_AX650X_DWMAC_H */ diff --git a/tests/qtest/ax650x-pyramid-test.c b/tests/qtest/ax650x-pyramid-test.c index 8b9b1a78bc53..3043d03c8d28 100644 --- a/tests/qtest/ax650x-pyramid-test.c +++ b/tests/qtest/ax650x-pyramid-test.c @@ -45,6 +45,62 @@ #define AX650X_EMMC_TEST_IMAGE_SIZE (1 * MiB) #define AX650X_EMMC_BLOCK_SIZE 512 +#define AX650X_DWMAC0_BASE 0x10140000 +#define AX650X_DWMAC1_BASE 0x30800000 +#define AX650X_DWMAC0_INTID (32 + 104) +#define AX650X_DWMAC0_GLB_BASE 0x10000000 +#define AX650X_DWMAC0_CLK_BASE 0x10010000 +#define AX650X_GPIO0_BASE 0x02003000 +#define AX650X_GPIO1_BASE 0x02004000 +#define AX650X_HWSPINLOCK_BASE 0x04510000 +#define AX650X_HWSPINLOCK_STRIDE 8 + +#define DWMAC_GMAC_VERSION 0x110 +#define DWMAC_GMAC_HW_FEATURE1 0x120 +#define DWMAC_GMAC_MDIO_ADDR 0x200 +#define DWMAC_GMAC_MDIO_DATA 0x204 +#define DWMAC_GMAC_VERSION_AX650X 0x1052 +#define DWMAC_HW_FEATURE1_ADDR64 (0x3 << 14) +#define DWMAC_HW_FEATURE1_ADDR64_40 BIT(14) +#define DWMAC_HW_FEATURE1_TSO BIT(18) +#define DWMAC_HW_FEATURE1_TXFIFO (0x1f << 6) +#define DWMAC_HW_FEATURE1_RXFIFO 0x1f +#define DWMAC_HW_FEATURE1_TXFIFO_32K (8 << 6) +#define DWMAC_HW_FEATURE1_RXFIFO_64K 9 +#define DWMAC_MDIO_BUSY BIT(0) +#define DWMAC_MDIO_GOC_WRITE (1 << 2) +#define DWMAC_MDIO_GOC_READ (3 << 2) +#define DWMAC_MDIO_REG(reg) ((reg) << 16) +#define DWMAC_MDIO_PHY(addr) ((addr) << 21) +#define DWMAC_BMCR_ANRESTART BIT(9) +#define DWMAC_BMCR_ANENABLE BIT(12) +#define DWMAC_BMCR_RESET BIT(15) + +#define DWMAC_DMA_CHAN_TX_CONTROL 0x1104 +#define DWMAC_DMA_CHAN_TX_BASE_HI 0x1110 +#define DWMAC_DMA_CHAN_TX_BASE 0x1114 +#define DWMAC_DMA_CHAN_TX_END_ADDR 0x1120 +#define DWMAC_DMA_CHAN_TX_RING_LEN 0x112c +#define DWMAC_DMA_CHAN_INTR_ENA 0x1134 +#define DWMAC_DMA_CHAN_STATUS 0x1160 +#define DWMAC_DMA_TX_START BIT(0) +#define DWMAC_DMA_STATUS_TI BIT(0) +#define DWMAC_DMA_STATUS_NIS BIT(15) + +#define DWMAC_DESC_OWN BIT(31) +#define DWMAC_TX_DESC_FIRST BIT(29) +#define DWMAC_TX_DESC_LAST BIT(28) +#define DWMAC_TX_DESC_IOC BIT(31) + +#define DW_GPIO_SWPORTA_DR 0x00 +#define DW_GPIO_SWPORTA_DDR 0x04 +#define DW_GPIO_EXT_PORTA 0x50 + +#define AX650X_DWMAC_GLB_PHY_IF 0x9c +#define AX650X_DWMAC_CLK_SW_RESET 0x10 +#define AX650X_DWMAC_CLK_RESET_SET 0x38 +#define AX650X_DWMAC_CLK_RESET_CLEAR 0x3c + #define SDHC_NORINTSTS 0x30 #define SDHC_NORINTSTSEN 0x34 #define SDHC_NORINTSIGEN 0x38 @@ -181,6 +237,168 @@ static void test_uart_extension_registers(void) qtest_quit(qts); } +static uint16_t dwmac_mdio_read(QTestState *qts, uint64_t base, + unsigned int phy, unsigned int reg) +{ + uint32_t command = DWMAC_MDIO_BUSY | DWMAC_MDIO_GOC_READ | + DWMAC_MDIO_PHY(phy) | DWMAC_MDIO_REG(reg); + + qtest_writel(qts, base + DWMAC_GMAC_MDIO_ADDR, command); + g_assert_cmphex(qtest_readl(qts, base + DWMAC_GMAC_MDIO_ADDR) & + DWMAC_MDIO_BUSY, ==, 0); + return qtest_readl(qts, base + DWMAC_GMAC_MDIO_DATA); +} + +static void dwmac_mdio_write(QTestState *qts, uint64_t base, + unsigned int phy, unsigned int reg, + uint16_t value) +{ + uint32_t command = DWMAC_MDIO_BUSY | DWMAC_MDIO_GOC_WRITE | + DWMAC_MDIO_PHY(phy) | DWMAC_MDIO_REG(reg); + + qtest_writel(qts, base + DWMAC_GMAC_MDIO_DATA, value); + qtest_writel(qts, base + DWMAC_GMAC_MDIO_ADDR, command); + g_assert_cmphex(qtest_readl(qts, base + DWMAC_GMAC_MDIO_ADDR) & + DWMAC_MDIO_BUSY, ==, 0); +} + +static void test_dwmac_registers_glue_and_reset(void) +{ + QTestState *qts = ax650x_pyramid_start(); + uint32_t feature; + + g_assert_cmphex(qtest_readl(qts, AX650X_DWMAC0_BASE + + DWMAC_GMAC_VERSION), ==, + DWMAC_GMAC_VERSION_AX650X); + g_assert_cmphex(qtest_readl(qts, AX650X_DWMAC1_BASE + + DWMAC_GMAC_VERSION), ==, + DWMAC_GMAC_VERSION_AX650X); + + feature = qtest_readl(qts, AX650X_DWMAC0_BASE + + DWMAC_GMAC_HW_FEATURE1); + g_assert_cmphex(feature & DWMAC_HW_FEATURE1_ADDR64, ==, + DWMAC_HW_FEATURE1_ADDR64_40); + g_assert_cmphex(feature & DWMAC_HW_FEATURE1_TSO, ==, + 0); + g_assert_cmphex(feature & DWMAC_HW_FEATURE1_TXFIFO, ==, + DWMAC_HW_FEATURE1_TXFIFO_32K); + g_assert_cmphex(feature & DWMAC_HW_FEATURE1_RXFIFO, ==, + DWMAC_HW_FEATURE1_RXFIFO_64K); + + g_assert_cmphex(dwmac_mdio_read(qts, AX650X_DWMAC0_BASE, 1, 2), ==, + 0x937c); + g_assert_cmphex(dwmac_mdio_read(qts, AX650X_DWMAC0_BASE, 1, 3), ==, + 0x4030); + g_assert_cmphex(dwmac_mdio_read(qts, AX650X_DWMAC0_BASE, 2, 2), ==, + 0xffff); + dwmac_mdio_write(qts, AX650X_DWMAC0_BASE, 1, 0, + DWMAC_BMCR_RESET | DWMAC_BMCR_ANENABLE | + DWMAC_BMCR_ANRESTART); + g_assert_cmphex(dwmac_mdio_read(qts, AX650X_DWMAC0_BASE, 1, 0), ==, + DWMAC_BMCR_ANENABLE); + + qtest_writel(qts, AX650X_GPIO0_BASE + DW_GPIO_SWPORTA_DDR, BIT(7)); + qtest_writel(qts, AX650X_GPIO0_BASE + DW_GPIO_SWPORTA_DR, BIT(7)); + g_assert_cmphex(qtest_readl(qts, AX650X_GPIO0_BASE + DW_GPIO_EXT_PORTA), + ==, BIT(7)); + qtest_writel(qts, AX650X_GPIO1_BASE + DW_GPIO_SWPORTA_DDR, BIT(11)); + qtest_writel(qts, AX650X_GPIO1_BASE + DW_GPIO_SWPORTA_DR, BIT(11)); + g_assert_cmphex(qtest_readl(qts, AX650X_GPIO1_BASE + DW_GPIO_EXT_PORTA), + ==, BIT(11)); + + qtest_writel(qts, AX650X_DWMAC0_GLB_BASE + AX650X_DWMAC_GLB_PHY_IF, + BIT(4)); + g_assert_cmphex(qtest_readl(qts, AX650X_DWMAC0_GLB_BASE + + AX650X_DWMAC_GLB_PHY_IF), ==, BIT(4)); + qtest_writel(qts, AX650X_DWMAC0_CLK_BASE + + AX650X_DWMAC_CLK_RESET_SET, BIT(7)); + g_assert_cmphex(qtest_readl(qts, AX650X_DWMAC0_CLK_BASE + + AX650X_DWMAC_CLK_SW_RESET), ==, BIT(7)); + qtest_writel(qts, AX650X_DWMAC0_CLK_BASE + + AX650X_DWMAC_CLK_RESET_CLEAR, BIT(7)); + g_assert_cmphex(qtest_readl(qts, AX650X_DWMAC0_CLK_BASE + + AX650X_DWMAC_CLK_SW_RESET), ==, 0); + + g_assert_cmphex(qtest_readl(qts, AX650X_HWSPINLOCK_BASE + + 12 * AX650X_HWSPINLOCK_STRIDE), ==, 0); + g_assert_cmphex(qtest_readl(qts, AX650X_HWSPINLOCK_BASE + + 12 * AX650X_HWSPINLOCK_STRIDE), ==, 1); + qtest_writel(qts, AX650X_HWSPINLOCK_BASE + + 12 * AX650X_HWSPINLOCK_STRIDE + sizeof(uint32_t), + 0xffffffff); + g_assert_cmphex(qtest_readl(qts, AX650X_HWSPINLOCK_BASE + + 12 * AX650X_HWSPINLOCK_STRIDE), ==, 0); + + qtest_system_reset(qts); + g_assert_cmphex(qtest_readl(qts, AX650X_DWMAC0_BASE + + DWMAC_GMAC_VERSION), ==, + DWMAC_GMAC_VERSION_AX650X); + g_assert_cmphex(qtest_readl(qts, AX650X_GPIO0_BASE + + DW_GPIO_EXT_PORTA), ==, 0); + g_assert_cmphex(qtest_readl(qts, AX650X_DWMAC0_GLB_BASE + + AX650X_DWMAC_GLB_PHY_IF), ==, 0); + + qtest_quit(qts); +} + +static void test_dwmac_40bit_dma_and_irq(void) +{ + const uint64_t desc_addr = AX650X_RAM_BASE + 2 * MiB; + const uint64_t frame_addr = desc_addr + 0x1000; + uint8_t frame[64] = { + 0x52, 0x54, 0x00, 0x12, 0x34, 0x56, + 0x52, 0x54, 0x00, 0x65, 0x43, 0x21, + 0x08, 0x00, + }; + uint32_t desc[4] = { + cpu_to_le32((uint32_t)frame_addr), + cpu_to_le32(frame_addr >> 32), + cpu_to_le32(DWMAC_TX_DESC_IOC | sizeof(frame)), + cpu_to_le32(DWMAC_DESC_OWN | DWMAC_TX_DESC_FIRST | + DWMAC_TX_DESC_LAST), + }; + QTestState *qts = ax650x_pyramid_start(); + uint64_t pending_addr; + uint32_t pending_mask; + uint32_t status; + + qtest_memwrite(qts, frame_addr, frame, sizeof(frame)); + qtest_memwrite(qts, desc_addr, desc, sizeof(desc)); + + qtest_writel(qts, AX650X_DWMAC0_BASE + DWMAC_DMA_CHAN_INTR_ENA, + DWMAC_DMA_STATUS_TI | DWMAC_DMA_STATUS_NIS); + qtest_writel(qts, AX650X_DWMAC0_BASE + DWMAC_DMA_CHAN_TX_BASE_HI, + desc_addr >> 32); + qtest_writel(qts, AX650X_DWMAC0_BASE + DWMAC_DMA_CHAN_TX_BASE, + (uint32_t)desc_addr); + qtest_writel(qts, AX650X_DWMAC0_BASE + DWMAC_DMA_CHAN_TX_RING_LEN, 0); + qtest_writel(qts, AX650X_DWMAC0_BASE + DWMAC_DMA_CHAN_TX_CONTROL, + DWMAC_DMA_TX_START); + qtest_writel(qts, AX650X_DWMAC0_BASE + DWMAC_DMA_CHAN_TX_END_ADDR, + (uint32_t)desc_addr); + + qtest_memread(qts, desc_addr, desc, sizeof(desc)); + g_assert_cmphex(le32_to_cpu(desc[3]) & DWMAC_DESC_OWN, ==, 0); + status = qtest_readl(qts, AX650X_DWMAC0_BASE + + DWMAC_DMA_CHAN_STATUS); + g_assert_cmphex(status & (DWMAC_DMA_STATUS_TI | DWMAC_DMA_STATUS_NIS), + ==, DWMAC_DMA_STATUS_TI | DWMAC_DMA_STATUS_NIS); + + pending_addr = AX650X_GIC_DIST_BASE + AX650X_GICD_ISPENDR + + (AX650X_DWMAC0_INTID / 32) * sizeof(uint32_t); + pending_mask = 1U << (AX650X_DWMAC0_INTID % 32); + g_assert_cmphex(qtest_readl(qts, pending_addr) & pending_mask, ==, + pending_mask); + + qtest_writel(qts, AX650X_DWMAC0_BASE + DWMAC_DMA_CHAN_STATUS, + DWMAC_DMA_STATUS_TI | DWMAC_DMA_STATUS_NIS); + g_assert_cmphex(qtest_readl(qts, AX650X_DWMAC0_BASE + + DWMAC_DMA_CHAN_STATUS) & + (DWMAC_DMA_STATUS_TI | DWMAC_DMA_STATUS_NIS), ==, 0); + + qtest_quit(qts); +} + static void test_emmc_registers_and_reset(void) { QTestState *qts = ax650x_pyramid_start(); @@ -393,6 +611,10 @@ int main(int argc, char **argv) test_uart_irq_and_reset); qtest_add_func("ax650x-pyramid/uart-extension-registers", test_uart_extension_registers); + qtest_add_func("ax650x-pyramid/dwmac-registers-glue-and-reset", + test_dwmac_registers_glue_and_reset); + qtest_add_func("ax650x-pyramid/dwmac-40bit-dma-and-irq", + test_dwmac_40bit_dma_and_irq); qtest_add_func("ax650x-pyramid/emmc-registers-and-reset", test_emmc_registers_and_reset); qtest_add_func("ax650x-pyramid/emmc-block-io-and-irq", From 7ccc5b90b481c0c67e92e932545c0ec2265bf209 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 21:20:36 +0800 Subject: [PATCH 12/14] tests/functional: add AX650X Ubuntu quick boot Signed-off-by: Chao Liu --- tests/functional/aarch64/meson.build | 2 + .../functional/aarch64/test_ax650x_ubuntu.py | 70 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/functional/aarch64/test_ax650x_ubuntu.py diff --git a/tests/functional/aarch64/meson.build b/tests/functional/aarch64/meson.build index d5f3246ad63a..23a4875dce25 100644 --- a/tests/functional/aarch64/meson.build +++ b/tests/functional/aarch64/meson.build @@ -4,6 +4,7 @@ test_aarch64_timeouts = { 'aspeed_ast2700a1' : 600, 'aspeed_ast2700a2' : 600, 'aspeed_ast2700fc' : 600, + 'ax650x_ubuntu' : 180, 'device_passthrough' : 720, 'imx8mm_evk' : 240, 'imx8mp_evk' : 240, @@ -31,6 +32,7 @@ tests_aarch64_system_thorough = [ 'aspeed_ast2700a1', 'aspeed_ast2700a2', 'aspeed_ast2700fc', + 'ax650x_ubuntu', 'device_passthrough', 'hotplug_pci', 'imx8mm_evk', diff --git a/tests/functional/aarch64/test_ax650x_ubuntu.py b/tests/functional/aarch64/test_ax650x_ubuntu.py new file mode 100644 index 000000000000..67c61856dbd7 --- /dev/null +++ b/tests/functional/aarch64/test_ax650x_ubuntu.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# +# Functional test that directly boots an AX650X Ubuntu eMMC image +# +# SPDX-License-Identifier: GPL-2.0-or-later + +from qemu_test import Asset, LinuxKernelTest + + +class AX650XUbuntuMachine(LinuxKernelTest): + + ASSET_URL = ( + 'https://github.com/processmission/qemu/releases/download/' + 'ax650x-ubuntu-22.04-qemu1/' + ) + ASSET_KERNEL = Asset( + ASSET_URL + 'Image-5.15.73-axera', + '9e3dc56e13cb5e786864d3456ceecfe7560d11fd25e9688531f86c0748be9bf8') + ASSET_EMMC = Asset( + ASSET_URL + 'ax650x-ubuntu-22.04-emmc.qcow2', + 'e362cff67d7358bc434394649f91d0b2424156eb9cd3b518438cf32fe3b9800c') + + PARTS = ( + 'mmcblk0:1536K(uboot),1536K(uboot_bk),1M(env),20M(param),' + '6M(logo),1M(dtb),64M(kernel),1M(atf),1M(optee),' + '1M(recovery_dtb),74M(recovery),30380032K(rootfs)' + ) + + def test_aarch64_ax650x_ubuntu_quick_boot(self): + kernel_path = self.ASSET_KERNEL.fetch() + image_path = self.ASSET_EMMC.fetch() + + self.set_machine('ax650x-pyramid') + self.require_accelerator('tcg') + self.vm.set_console() + + kernel_command_line = ( + 'console=ttyS0,115200n8 ' + 'earlycon=uart8250,mmio32,0x2016000 ' + 'root=/dev/mmcblk0p12 rootfstype=ext4 rw rootwait ' + f'blkdevparts={self.PARTS} ' + 'systemd.show_status=yes systemd.log_target=console' + ) + self.vm.add_args( + '-accel', 'tcg,thread=multi', + '-cpu', 'cortex-a55', + '-smp', '8', + '-m', '2G', + '-kernel', kernel_path, + '-append', kernel_command_line, + '-drive', f'file={image_path},if=sd,format=qcow2,snapshot=on', + '-display', 'none', + '-monitor', 'none', + '-no-reboot', + ) + + self.vm.launch() + self.wait_for_console_pattern('DWMAC4/5') + self.wait_for_console_pattern('p12(rootfs)') + self.wait_for_console_pattern( + 'Mounted root (ext4 filesystem) on device 179:12') + self.wait_for_console_pattern('AX650X_UBUNTU_READY') + self.wait_for_console_pattern('AX650X_OS=Ubuntu 22.04.5 LTS') + self.wait_for_console_pattern('AX650X_CPUS=8') + self.wait_for_console_pattern('AX650X_ROOT=/dev/mmcblk0p12 ext4') + self.wait_for_console_pattern('localhost login:') + + +if __name__ == '__main__': + LinuxKernelTest.main() From f98e76d5a3a1217d5dc6b13975b2a130f706aa45 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 21:23:08 +0800 Subject: [PATCH 13/14] docs/system/arm: document AX650X Ubuntu boot Signed-off-by: Chao Liu --- README.md | 75 ++++++++++++++++++++++++++ docs/system/arm/ax650x-pyramid.rst | 84 +++++++++++++++++++++++------- 2 files changed, 141 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 20883ac8ea65..75b384e57113 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ for new machine requests or bug reports. | 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 | [`ax650x-pyramid`](docs/system/arm/ax650x-pyramid.rst) | ✅ | ❌ | PM | | ARM | [`phytium-pi`](docs/system/arm/phytium-pi.rst) | ✅ | ✅ | PM | | ARM | [`rk3588-evb`](docs/system/arm/rk3588.rst) | ✅ | ✅ | PM | | ARM | [`rk3588s-roc-pc`](docs/system/arm/rk3588.rst) | ✅ | ✅ | PM | @@ -34,3 +35,77 @@ It provides agent skills for planning, register extraction, peripheral modeling, board modeling, qtest, build, debugging, and verification. - Skill repository: + +## AX650X Pyramid quick start + +The `ax650x-pyramid` machine directly boots Linux on the M5Stack AI Pyramid / +AXERA AX650X platform. Detailed machine documentation is available in +[docs/system/arm/ax650x-pyramid.rst](docs/system/arm/ax650x-pyramid.rst). + +### Boot Ubuntu 22.04 from eMMC + +The Ubuntu image has no MBR or GPT. Linux creates its twelve partitions from +the fixed `blkdevparts` command line. The command below uses `snapshot=on`, so +guest writes are discarded when QEMU exits. + +```sh +QEMU=${QEMU:-build/qemu-system-aarch64} +ASSET_DIR=${ASSET_DIR:-assets/ax650x} +KERNEL=${KERNEL:-$ASSET_DIR/Image-5.15.73-axera} +EMMC=${EMMC:-$ASSET_DIR/ax650x-ubuntu-22.04-emmc.raw} + +PARTS='mmcblk0:1536K(uboot),1536K(uboot_bk),1M(env),20M(param)' +PARTS="$PARTS,6M(logo),1M(dtb),64M(kernel),1M(atf),1M(optee)" +PARTS="$PARTS,1M(recovery_dtb),74M(recovery),30380032K(rootfs)" +CMDLINE='console=ttyS0,115200n8 earlycon=uart8250,mmio32,0x2016000' +CMDLINE="$CMDLINE root=/dev/mmcblk0p12 rootfstype=ext4 rw rootwait" +CMDLINE="$CMDLINE blkdevparts=$PARTS" +CMDLINE="$CMDLINE systemd.show_status=yes systemd.log_target=console" + +for input in "$QEMU" "$KERNEL" "$EMMC"; do + if [ ! -r "$input" ]; then + echo "missing input: $input" >&2 + exit 1 + fi +done + +exec "$QEMU" \ + -machine ax650x-pyramid \ + -accel tcg,thread=multi \ + -cpu cortex-a55 \ + -smp 8 \ + -m 2G \ + -kernel "$KERNEL" \ + -append "$CMDLINE" \ + -drive "file=$EMMC,if=sd,format=raw,snapshot=on" \ + -chardev stdio,id=serial0,signal=off \ + -serial chardev:serial0 \ + -display none \ + -monitor none \ + -no-reboot +``` + +Override `QEMU`, `KERNEL`, or `EMMC` in the environment when the artifacts +live elsewhere. Remove `snapshot=on` only when persistent image writes are +intentional. + +### Run the Ubuntu quick-boot functional test + +The functional test uses the same direct-boot contract but lets the test +harness own the serial chardev. It is in the `thorough` suite because the +kernel and compressed eMMC image are downloaded assets. + +```sh +meson test -C build \ + --suite thorough \ + func-aarch64-ax650x_ubuntu \ + --print-errorlogs +``` + +The pinned kernel and qcow2 image are published at: + + + +The harness verifies both SHA-256 digests. The test uses disposable eMMC +writes and waits for DWMAC probe, partition 12, the mounted ext4 root +filesystem, Ubuntu readiness markers, and the serial login prompt. diff --git a/docs/system/arm/ax650x-pyramid.rst b/docs/system/arm/ax650x-pyramid.rst index 0715f5d8aecc..a7cb9662bdf4 100644 --- a/docs/system/arm/ax650x-pyramid.rst +++ b/docs/system/arm/ax650x-pyramid.rst @@ -27,9 +27,13 @@ The machine provides the following devices and architectural services: * Arm generic timers running at 24 MHz; * PSCI 1.0 using the SMC conduit; * the AXERA UART0 console at ``0x02016000``, including the extension - registers needed by the AXERA 8250 driver; and + registers needed by the AXERA 8250 driver; * an AX650X SDHCI/eMMC controller at ``0x28000000``, including the vendor PHY - and eMMC registers used during Linux probe. + and eMMC registers used during Linux probe; +* two Synopsys DWMAC 4.10a compatible Ethernet controllers, including the + AX650X clock/reset glue, MDIO PHY identity, DMA and interrupt paths; +* the DesignWare APB GPIO blocks used for PHY reset; and +* the AX650X hardware spinlock block used by the GPIO driver. QEMU generates a minimal device tree containing only those implemented devices. An eMMC backend supplied with ``if=sd`` is attached to the AX650X @@ -42,19 +46,44 @@ The model requires an uncompressed Arm64 ``Image`` containing the AXERA UART and SDHCI drivers. The following example boots an ext4 root filesystem from partition 12 of an eMMC image that uses the target's fixed partition layout. The image has no MBR or GPT, so the complete ``blkdevparts`` argument is -required:: - - $ KERNEL=/path/to/Image-5.15.73-axera - $ EMMC=/path/to/ax650x-ubuntu-22.04-emmc.raw - $ PARTS='mmcblk0:1536K(uboot),1536K(uboot_bk),1M(env),20M(param),6M(logo),1M(dtb),64M(kernel),1M(atf),1M(optee),1M(recovery_dtb),74M(recovery),30380032K(rootfs)' - $ qemu-system-aarch64 \ +required: + +.. code-block:: shell + + QEMU=${QEMU:-build/qemu-system-aarch64} + ASSET_DIR=${ASSET_DIR:-assets/ax650x} + KERNEL=${KERNEL:-$ASSET_DIR/Image-5.15.73-axera} + EMMC=${EMMC:-$ASSET_DIR/ax650x-ubuntu-22.04-emmc.raw} + + PARTS='mmcblk0:1536K(uboot),1536K(uboot_bk),1M(env),20M(param)' + PARTS="$PARTS,6M(logo),1M(dtb),64M(kernel),1M(atf),1M(optee)" + PARTS="$PARTS,1M(recovery_dtb),74M(recovery),30380032K(rootfs)" + CMDLINE='console=ttyS0,115200n8 earlycon=uart8250,mmio32,0x2016000' + CMDLINE="$CMDLINE root=/dev/mmcblk0p12 rootfstype=ext4 rw rootwait" + CMDLINE="$CMDLINE blkdevparts=$PARTS" + CMDLINE="$CMDLINE systemd.show_status=yes systemd.log_target=console" + + for input in "$QEMU" "$KERNEL" "$EMMC"; do + if [ ! -r "$input" ]; then + echo "missing input: $input" >&2 + exit 1 + fi + done + + exec "$QEMU" \ -machine ax650x-pyramid \ -accel tcg,thread=multi \ - -smp 8 -m 2G \ + -cpu cortex-a55 \ + -smp 8 \ + -m 2G \ -kernel "$KERNEL" \ - -append "console=ttyS0,115200n8 earlycon=uart8250,mmio32,0x2016000 root=/dev/mmcblk0p12 rootfstype=ext4 rw rootwait blkdevparts=$PARTS" \ - -drive file="$EMMC",if=sd,format=raw,snapshot=on \ - -serial mon:stdio -display none -no-reboot + -append "$CMDLINE" \ + -drive "file=$EMMC,if=sd,format=raw,snapshot=on" \ + -chardev stdio,id=serial0,signal=off \ + -serial chardev:serial0 \ + -display none \ + -monitor none \ + -no-reboot ``snapshot=on`` keeps the reusable eMMC image unchanged. Remove it only when persistent guest writes are intentional. @@ -69,11 +98,11 @@ Known limitations * Only QEMU direct kernel boot is implemented. BootROM, SPL, U-Boot, Arm Trusted Firmware and OP-TEE images are not loaded or executed. -* The AXERA DWMAC 4.10a Ethernet controllers are not modeled, so this machine - currently has no network interface. * NPU, VDSP, RISC-V auxiliary cores, video, ISP, display, audio, USB, PCIe, - SATA, GPIO and the full clock, reset and power-management trees are not - modeled. + SATA and the full clock, reset and power-management trees are not modeled. + GPIO behavior is limited to the DesignWare APB subset needed for PHY reset. +* DWMAC TSO, PTP/TSN, multi-queue performance fidelity and analog PHY timing + are not modeled. * The UART extension window implements the probe-time subset. The AXERA driver can report a harmless capability mismatch because the UART component version register intentionally selects its compatible fallback path. @@ -87,8 +116,27 @@ Running tests ------------- The board qtest covers CPU topology, RAM, GICv2, UART MMIO and IRQ behavior, -SDHCI vendor registers, reset, block I/O, eMMC IRQ routing, 64-bit ADMA -capability and Auto CMD23:: +SDHCI vendor registers, reset, block I/O, eMMC IRQ routing, 64-bit ADMA, +Auto CMD23, DWMAC synthesis registers, MDIO, GPIO and reset glue, hardware +spinlocks, 40-bit DMA and Ethernet IRQ routing:: $ meson test -C build qemu:qtest-aarch64/ax650x-pyramid-test \ --print-errorlogs + +The Ubuntu quick-boot functional test downloads a pinned kernel and compressed +qcow2 image from the `AX650X Ubuntu 22.04 QEMU assets release +`__. +The functional asset layer verifies both SHA-256 digests. The test uses +``snapshot=on`` and waits for DWMAC probe, the eMMC partition map, the mounted +root filesystem, Ubuntu readiness markers and the serial login prompt: + +.. code-block:: shell + + $ meson test -C build \ + --suite thorough \ + func-aarch64-ax650x_ubuntu \ + --print-errorlogs + +The test is registered in the ``thorough`` functional suite because the +kernel and Ubuntu image are external assets. A cached copy is reused only +after its declared content hash has been checked. From 09d44348e7b7a27d5b030ac9e71079480d645cf9 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Wed, 15 Jul 2026 21:29:11 +0800 Subject: [PATCH 14/14] MAINTAINERS: add AX650X Pyramid machine Signed-off-by: Chao Liu --- MAINTAINERS | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 912900615ef3..c4e25ea9412c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -882,6 +882,22 @@ F: include/hw/misc/arm_integrator_debug.h F: tests/functional/arm/test_integratorcp.py F: docs/system/arm/integratorcp.rst +M5Stack AI Pyramid (AX650X) +M: Chao Liu +L: qemu-arm@nongnu.org +S: Maintained +F: docs/system/arm/ax650x-pyramid.rst +F: hw/arm/ax650x*.c +F: hw/gpio/dw-apb-gpio.c +F: hw/misc/ax650x-*.c +F: hw/sd/ax650x-sdhci.c +F: include/hw/arm/ax650x*.h +F: include/hw/gpio/dw-apb-gpio.h +F: include/hw/misc/ax650x-*.h +F: include/hw/sd/ax650x-sdhci.h +F: tests/functional/aarch64/test_ax650x_ubuntu.py +F: tests/qtest/ax650x-pyramid-test.c + MAX78000FTHR L: qemu-arm@nongnu.org S: Orphan