[PATCH v1 3/3] tests/qtest: add K230 SDHCI tests

Xin Xie <[email protected]> Wed, 29 Jul 2026 18:59:03 +0800
Newsgroups org.nongnu.qemu-riscv,org.nongnu.qemu-devel
Message-ID <[email protected]>
Add qtests covering the K230-specific SDHCI register layout, capability
masking, preset values, extension pointers, PHY power status and
per-controller vendor register storage.

Verify card detection and interrupt delivery through the K230 PLIC.
Exercise basic PIO reads and writes with an attached SD card image and
check that written data reaches the backing file.

Signed-off-by: Xin Xie <[email protected]>
---
 MAINTAINERS                   |   1 +
 tests/qtest/k230-sdhci-test.c | 214 ++++++++++++++++++++++++++++++++++
 tests/qtest/meson.build       |   5 +-
 3 files changed, 219 insertions(+), 1 deletion(-)
 create mode 100644 tests/qtest/k230-sdhci-test.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 06285352c4..99c4c3294b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1831,6 +1831,7 @@ F: include/hw/riscv/k230.h
 F: include/hw/watchdog/k230_wdt.h
 F: include/hw/sd/k230_sdhci.h
 F: tests/qtest/k230-wdt-test.c
+F: tests/qtest/k230-sdhci-test.c
 
 RX Machines
 -----------
diff --git a/tests/qtest/k230-sdhci-test.c b/tests/qtest/k230-sdhci-test.c
new file mode 100644
index 0000000000..5eedb130c3
--- /dev/null
+++ b/tests/qtest/k230-sdhci-test.c
@@ -0,0 +1,214 @@
+/*
+ * QTest testcase for the Kendryte K230 SDHCI controllers
+ *
+ * Copyright (c) 2026 Xin Xie <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/timer.h"
+#include "qemu/units.h"
+#include "libqtest.h"
+#include "libqos/sdhci-cmd.h"
+#include "hw/sd/k230_sdhci.h"
+#include "hw/sd/sdhci-internal.h"
+
+#define K230_SD0_BASE          0x91580000
+#define K230_SD1_BASE          0x91581000
+#define K230_PLIC_PENDING      0xf00001000ULL
+#define K230_SD1_IRQ           144
+
+#define K230_SD_TEST_IMAGE_SIZE (2 * MiB)
+#define K230_SD_TEST_BLOCK_SIZE 512
+
+static void test_registers(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+    uint64_t capabilities;
+    uint64_t expected = K230_SDHCI_CAPAREG_RESET;
+
+    /* Only capabilities unsupported by the QEMU model are removed. */
+    expected = FIELD_DP64(expected, SDHC_CAPAB, ASYNC_INT, 0);
+    expected = FIELD_DP64(expected, SDHC_CAPAB, TIMER_RETUNING, 0);
+    expected = FIELD_DP64(expected, SDHC_CAPAB, RETUNING_MODE, 0);
+    expected = FIELD_DP64(expected, SDHC_CAPAB, ADMA3, 0);
+    capabilities = qtest_readq(qts, K230_SD0_BASE + SDHC_CAPAB);
+    g_assert_cmphex(capabilities, ==, expected);
+
+    /* Preset Value registers use the TRM reset values and are read-only. */
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_PRESET_INIT) ==
+             K230_SDHCI_PRESET_INIT_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE +
+                         K230_SDHCI_PRESET_DEFAULT_SPEED) ==
+             K230_SDHCI_PRESET_DEFAULT_SPEED_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE +
+                         K230_SDHCI_PRESET_HIGH_SPEED) ==
+             K230_SDHCI_PRESET_HIGH_SPEED_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_PRESET_SDR12) ==
+             K230_SDHCI_PRESET_SDR12_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_PRESET_SDR25) ==
+             K230_SDHCI_PRESET_SDR25_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_PRESET_SDR50) ==
+             K230_SDHCI_PRESET_SDR50_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_PRESET_SDR104) ==
+             K230_SDHCI_PRESET_SDR104_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_PRESET_DDR50) ==
+             K230_SDHCI_PRESET_DDR50_RESET);
+
+    qtest_writew(qts, K230_SD0_BASE + K230_SDHCI_PRESET_SDR50, 0xffff);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_PRESET_SDR50) ==
+             K230_SDHCI_PRESET_SDR50_RESET);
+
+    /* Extension-area pointers use their documented read-only reset values. */
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_P_UHS2_SETTINGS) ==
+             K230_SDHCI_P_UHS2_SETTINGS_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_P_UHS2_HOST_CAPAB) ==
+             K230_SDHCI_P_UHS2_HOST_CAPAB_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_P_UHS2_TEST) ==
+             K230_SDHCI_P_UHS2_TEST_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_P_EMBEDDED_CNTRL) ==
+             K230_SDHCI_P_EMBEDDED_CNTRL_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_P_VENDOR_AREA1) ==
+             K230_SDHCI_P_VENDOR_AREA1_RESET);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_P_VENDOR_AREA2) ==
+             K230_SDHCI_P_VENDOR_AREA2_RESET);
+
+    qtest_writew(qts, K230_SD0_BASE + K230_SDHCI_P_VENDOR_AREA1, 0xffff);
+    g_assert(qtest_readw(qts, K230_SD0_BASE + K230_SDHCI_P_VENDOR_AREA1) ==
+             K230_SDHCI_P_VENDOR_AREA1_RESET);
+
+    /* The virtual PHY is always ready and software cannot clear PWRGOOD. */
+    g_assert((qtest_readl(qts, K230_SD0_BASE + K230_SDHCI_PHY_CNFG) &
+              K230_SDHCI_PHY_CNFG_PWRGOOD) == K230_SDHCI_PHY_CNFG_PWRGOOD);
+    qtest_writel(qts, K230_SD0_BASE + K230_SDHCI_PHY_CNFG, 0);
+    g_assert(qtest_readl(qts, K230_SD0_BASE + K230_SDHCI_PHY_CNFG) ==
+             K230_SDHCI_PHY_CNFG_PWRGOOD);
+
+    /* Vendor storage is writable and private to each controller instance. */
+    qtest_writel(qts, K230_SD0_BASE + K230_SDHCI_P_VENDOR_AREA1_RESET,
+                 0x89abcdef);
+    g_assert(qtest_readl(qts, K230_SD0_BASE +
+                         K230_SDHCI_P_VENDOR_AREA1_RESET) ==
+             0x89abcdef);
+    g_assert(qtest_readl(qts, K230_SD1_BASE +
+                         K230_SDHCI_P_VENDOR_AREA1_RESET) == 0);
+
+    qtest_quit(qts);
+}
+
+static void test_card_detect(void)
+{
+    g_autofree char *path = NULL;
+    int fd = g_file_open_tmp("k230-sd-XXXXXX.img", &path, NULL);
+    QTestState *qts;
+
+    g_assert(fd >= 0);
+    g_assert(ftruncate(fd, K230_SD_TEST_IMAGE_SIZE) == 0);
+    close(fd);
+
+    qts = qtest_initf("-machine k230 "
+                      "-drive if=sd,index=1,format=raw,file=%s", path);
+
+    /* Card insertion becomes visible after the generic insertion delay. */
+    qtest_clock_step(qts, NANOSECONDS_PER_SECOND);
+    g_assert((qtest_readl(qts, K230_SD1_BASE + SDHC_PRNSTS) &
+              SDHC_CARD_PRESENT) == SDHC_CARD_PRESENT);
+    g_assert((qtest_readl(qts, K230_SD0_BASE + SDHC_PRNSTS) &
+              SDHC_CARD_PRESENT) == 0);
+
+    /* Route a command-complete event through SDHCI to the K230 PLIC. */
+    qtest_writew(qts, K230_SD1_BASE + SDHC_NORINTSTSEN,
+                 SDHC_NISEN_CMDCMP);
+    qtest_writew(qts, K230_SD1_BASE + SDHC_NORINTSIGEN,
+                 SDHC_NISEN_CMDCMP);
+    g_assert((qtest_readw(qts, K230_SD1_BASE + SDHC_NORINTSTSEN) &
+              SDHC_NISEN_CMDCMP) != 0);
+    g_assert((qtest_readw(qts, K230_SD1_BASE + SDHC_NORINTSIGEN) &
+              SDHC_NISEN_CMDCMP) != 0);
+    qtest_writew(qts, K230_SD1_BASE + SDHC_CLKCON,
+                 SDHC_CLOCK_INT_EN | SDHC_CLOCK_SDCLK_EN);
+    qtest_writew(qts, K230_SD1_BASE + SDHC_CMDREG,
+                 SDHC_APP_CMD | SDHC_CMD_RESPONSE);
+    g_assert((qtest_readw(qts, K230_SD1_BASE + SDHC_NORINTSTS) &
+              SDHC_NIS_CMDCMP) != 0);
+    g_assert((qtest_readl(qts, K230_PLIC_PENDING +
+                          K230_SD1_IRQ / 32 * sizeof(uint32_t)) &
+              BIT(K230_SD1_IRQ % 32)) != 0);
+
+    qtest_quit(qts);
+    unlink(path);
+}
+
+static void k230_sdhci_select_card(QTestState *qts)
+{
+    uint16_t rca;
+
+    /* Reset the controller and enable its internal and SD clocks. */
+    qtest_writeb(qts, K230_SD1_BASE + SDHC_SWRST, SDHC_RESET_ALL);
+    qtest_writew(qts, K230_SD1_BASE + SDHC_CLKCON,
+                 SDHC_CLOCK_INT_EN | SDHC_CLOCK_INT_STABLE |
+                 SDHC_CLOCK_SDCLK_EN);
+
+    /* Run the SD identification sequence and select the assigned RCA. */
+    sdhci_cmd_regs(qts, K230_SD1_BASE, 0, 0, 0, 0, SDHC_APP_CMD);
+    sdhci_cmd_regs(qts, K230_SD1_BASE, 0, 0, 0x41200000, 0, 41 << 8);
+    sdhci_cmd_regs(qts, K230_SD1_BASE, 0, 0, 0, 0, SDHC_ALL_SEND_CID);
+    sdhci_cmd_regs(qts, K230_SD1_BASE, 0, 0, 0, 0,
+                   SDHC_SEND_RELATIVE_ADDR | SDHC_CMD_RESPONSE);
+    rca = qtest_readl(qts, K230_SD1_BASE + SDHC_RSPREG0) >> 16;
+    sdhci_cmd_regs(qts, K230_SD1_BASE, 0, 0, rca << 16, 0,
+                   SDHC_SELECT_DESELECT_CARD);
+}
+
+static void test_card_io(void)
+{
+    static const char read_data[] = "K230 SDHCI read";
+    static const char write_data[] = "K230 SDHCI write";
+    g_autofree char *path = NULL;
+    char buffer[K230_SD_TEST_BLOCK_SIZE];
+    int fd = g_file_open_tmp("k230-sd-XXXXXX.img", &path, NULL);
+    QTestState *qts;
+
+    /* Seed block zero so the controller read can be checked independently. */
+    g_assert(fd >= 0);
+    g_assert(ftruncate(fd, K230_SD_TEST_IMAGE_SIZE) == 0);
+    g_assert(pwrite(fd, read_data, sizeof(read_data), 0) == sizeof(read_data));
+    close(fd);
+
+    qts = qtest_initf("-machine k230 "
+                      "-drive if=sd,index=1,format=raw,auto-read-only=off,"
+                      "file=%s", path);
+    k230_sdhci_select_card(qts);
+
+    /* Read block zero through the SDHCI PIO data port. */
+    memset(buffer, 0, sizeof(buffer));
+    g_assert(sdhci_read_cmd(qts, K230_SD1_BASE, buffer,
+                            sizeof(read_data)) == sizeof(read_data));
+    g_assert(memcmp(buffer, read_data, sizeof(read_data)) == 0);
+
+    /* Write through SDHCI, then verify the data reached the backing image. */
+    sdhci_write_cmd(qts, K230_SD1_BASE, write_data, sizeof(write_data),
+                    K230_SD_TEST_BLOCK_SIZE);
+    qtest_quit(qts);
+
+    fd = open(path, O_RDONLY);
+    g_assert(fd >= 0);
+    memset(buffer, 0, sizeof(buffer));
+    g_assert(pread(fd, buffer, sizeof(write_data), 0) == sizeof(write_data));
+    close(fd);
+    g_assert(memcmp(buffer, write_data, sizeof(write_data)) == 0);
+
+    unlink(path);
+}
+
+int main(int argc, char *argv[])
+{
+    g_test_init(&argc, &argv, NULL);
+
+    qtest_add_func("/k230-sdhci/registers", test_registers);
+    qtest_add_func("/k230-sdhci/card-detect", test_card_detect);
+    qtest_add_func("/k230-sdhci/card-io", test_card_io);
+
+    return g_test_run();
+}
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 56ff860e21..0600b1e807 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -297,7 +297,10 @@ qtests_riscv64 = ['riscv-csr-test'] + \
   (config_all_devices.has_key('CONFIG_IOMMU_TESTDEV') and
    config_all_devices.has_key('CONFIG_RISCV_IOMMU') ?
    ['iommu-riscv-test'] : []) + \
-  (config_all_devices.has_key('CONFIG_K230') ? ['k230-wdt-test'] : [])
+  (config_all_devices.has_key('CONFIG_K230') ? [
+    'k230-wdt-test',
+    'k230-sdhci-test',
+  ] : [])
 
 qtests_hexagon = ['boot-serial-test']
 
-- 
2.43.0