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']