Add an image-free qtest for the microchip-icicle-kit machine that drives
the generic SDHCI model (hw/sd/sdhci.c) through the Cadence wrapper
(hw/sd/cadence_sdhci.c, SRS window at 0x20008200) directly over MMIO
under "-accel qtest", where QEMU_CLOCK_VIRTUAL only advances when the
test steps it.
It reproduces two aspects of the ADMA2 engine objectively, without any
guest firmware:
1. Pacing: a 12-descriptor (> SDHC_ADMA_DESCS_PER_DELAY) non-interrupt
ADMA2 CMD18 read completes after a model-dependent number of
SDHC_TRANSFER_DELAY virtual-clock steps, which the test reports.
2. MMIO coupling: a chain carrying a DMA-boundary interrupt descriptor
yields mid-chain and reschedules on the virtual clock. With the
clock frozen, a bare interrupt-status read must not itself resume or
complete the transfer. This asserts the behaviour intended by "Run
ADMA independently of MMIO": it passes on the fixed model and fails
on a model that resumes ADMA from an MMIO read.
The SD bring-up (CMD0/CMD8/ACMD41/CMD2/CMD3/CMD7/CMD16) and the ADMA2
descriptor-table layout mirror the pattern already used by the SDHCI
device model; SDHCI register offsets and ADMA attribute bits are
reproduced locally because sdhci-internal.h is not standalone-includable
from a qtest.
Signed-off-by: Wadim Mueller <[email protected]>
---
MAINTAINERS | 1 +
tests/qtest/cadence-sdhci-test.c | 268 +++++++++++++++++++++++++++++++
tests/qtest/meson.build | 1 +
3 files changed, 270 insertions(+)
create mode 100644 tests/qtest/cadence-sdhci-test.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 63e9ba521b..12c657d000 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2277,6 +2277,7 @@ F: include/hw/sd/sd*
F: hw/sd/core.c
F: hw/sd/sd*
F: hw/sd/ssi-sd.c
+F: tests/qtest/cadence-sdhci-test.c
F: tests/qtest/fuzz-sdcard-test.c
F: tests/qtest/sdhci-test.c
diff --git a/tests/qtest/cadence-sdhci-test.c b/tests/qtest/cadence-sdhci-test.c
new file mode 100644
index 0000000000..f2f06fdcaf
--- /dev/null
+++ b/tests/qtest/cadence-sdhci-test.c
@@ -0,0 +1,268 @@
+/*
+ * QTest for Cadence SDHCI ADMA2 pacing / MMIO coupling on the Microchip
+ * PolarFire SoC Icicle Kit (microchip-icicle-kit).
+ *
+ * Drives the generic SDHCI model (hw/sd/sdhci.c) through the Cadence wrapper
+ * directly over MMIO under "-accel qtest", where QEMU_CLOCK_VIRTUAL only
+ * advances when the test steps it; no guest firmware runs.
+ *
+ * Copyright (c) 2026 Wadim Mueller <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+#include "qemu/osdep.h"
+#include "libqtest.h"
+
+/*
+ * SDHCI register offsets and bits, mirrored from hw/sd/sdhci-internal.h,
+ * which is not standalone-includable from a qtest.
+ */
+#define SDHC_BLKSIZE 0x04
+#define SDHC_ARGUMENT 0x08
+#define SDHC_TRNMOD 0x0C
+#define SDHC_TRNS_DMA 0x0001
+#define SDHC_TRNS_BLK_CNT_EN 0x0002
+#define SDHC_TRNS_READ 0x0010
+#define SDHC_TRNS_MULTI 0x0020
+#define SDHC_CMDREG 0x0E
+#define SDHC_CMD_DATA_PRESENT (1 << 5)
+#define SDHC_RSPREG0 0x10
+#define SDHC_PRNSTS 0x24
+#define SDHC_CARD_PRESENT 0x00010000
+#define SDHC_HOSTCTL 0x28
+#define SDHC_CTRL_ADMA2_32 0x10
+#define SDHC_CLKCON 0x2C
+#define SDHC_CLOCK_INT_EN 0x0001
+#define SDHC_CLOCK_SDCLK_EN (1 << 2)
+#define SDHC_SWRST 0x2F
+#define SDHC_RESET_ALL 0x01
+#define SDHC_NORINTSTS 0x30
+#define SDHC_NIS_ERR 0x8000
+#define SDHC_NIS_CMDCMP 0x0001
+#define SDHC_NIS_TRSCMP 0x0002
+#define SDHC_NIS_DMA 0x0008
+#define SDHC_NORINTSTSEN 0x34
+#define SDHC_ERRINTSTSEN 0x36
+#define SDHC_NORINTSIGEN 0x38
+#define SDHC_ADMASYSADDR 0x58
+#define SDHC_ADMA_ATTR_ACT_TRAN (1 << 5)
+#define SDHC_ADMA_ATTR_INT (1 << 2)
+#define SDHC_ADMA_ATTR_END (1 << 1)
+#define SDHC_ADMA_ATTR_VALID (1 << 0)
+#define SDHC_TRANSFER_DELAY 100 /* ns between ADMA batches */
+
+/* Cadence maps the generic SDHCI (SRS) register window at base + 0x200. */
+#define SDHCI_BASE (0x20008000ULL + 0x200)
+
+/* Scratch in PolarFire DRAM (base 0x80000000): descriptor table + target. */
+#define ADMA_TABLE_ADDR 0x82000000ULL
+#define ADMA_BUF_ADDR 0x82100000ULL
+
+#define BLK_LEN 512
+#define NDESC 12 /* > SDHC_ADMA_DESCS_PER_DELAY (5) */
+#define INT_DESC 5 /* DMA-boundary interrupt descriptor */
+
+/* CMDREG response-type encodings (bits [1:0]). */
+#define RESP_NONE 0x0000
+#define RESP_R2 0x0001 /* 136-bit */
+#define RESP_R48 0x0002 /* 48-bit */
+#define RESP_R1B 0x0003 /* 48-bit with busy */
+
+#define ACMD41_ARG 0x40FF8000u /* HCS + 3.3-3.6V window */
+#define CMD8_ARG 0x000001AAu /* VHS 2.7-3.6V + check pattern */
+
+/* Issue an SD command and wait (bounded) for Command Complete. */
+static uint16_t sd_cmd(QTestState *qts, uint8_t cmd, uint32_t arg,
+ uint16_t flags)
+{
+ uint16_t sts = 0;
+ int i;
+
+ qtest_writel(qts, SDHCI_BASE + SDHC_ARGUMENT, arg);
+ qtest_writew(qts, SDHCI_BASE + SDHC_CMDREG, ((uint16_t)cmd << 8) | flags);
+
+ for (i = 0; i < 1000; i++) {
+ sts = qtest_readw(qts, SDHCI_BASE + SDHC_NORINTSTS);
+ if (sts & SDHC_NIS_CMDCMP) {
+ break;
+ }
+ }
+ g_assert_cmphex(sts & SDHC_NIS_CMDCMP, ==, SDHC_NIS_CMDCMP);
+
+ qtest_writew(qts, SDHCI_BASE + SDHC_NORINTSTS, 0xffff); /* w1c */
+ return sts;
+}
+
+/* Bring the SD card from idle to transfer state. */
+static void sd_bring_up_card(QTestState *qts)
+{
+ uint32_t rca;
+
+ g_assert_cmphex(qtest_readl(qts, SDHCI_BASE + SDHC_PRNSTS) &
+ SDHC_CARD_PRESENT, ==, SDHC_CARD_PRESENT);
+
+ qtest_writeb(qts, SDHCI_BASE + SDHC_SWRST, SDHC_RESET_ALL);
+ qtest_writew(qts, SDHCI_BASE + SDHC_CLKCON,
+ SDHC_CLOCK_INT_EN | SDHC_CLOCK_SDCLK_EN);
+ qtest_writew(qts, SDHCI_BASE + SDHC_NORINTSTSEN, 0xffff);
+ qtest_writew(qts, SDHCI_BASE + SDHC_ERRINTSTSEN, 0xffff);
+ qtest_writew(qts, SDHCI_BASE + SDHC_NORINTSIGEN, 0xffff);
+
+ sd_cmd(qts, 0, 0x00000000, RESP_NONE); /* GO_IDLE_STATE */
+ sd_cmd(qts, 8, CMD8_ARG, RESP_R48); /* SEND_IF_COND */
+ sd_cmd(qts, 55, 0x00000000, RESP_R48); /* APP_CMD */
+ sd_cmd(qts, 41, ACMD41_ARG, RESP_R48); /* SD_SEND_OP_COND */
+ sd_cmd(qts, 2, 0x00000000, RESP_R2); /* ALL_SEND_CID */
+ sd_cmd(qts, 3, 0x00000000, RESP_R48); /* SEND_RELATIVE_ADDR */
+
+ rca = qtest_readl(qts, SDHCI_BASE + SDHC_RSPREG0) >> 16; /* R6 */
+
+ sd_cmd(qts, 7, rca << 16, RESP_R1B); /* SELECT_CARD */
+ sd_cmd(qts, 16, BLK_LEN, RESP_R48); /* SET_BLOCKLEN */
+}
+
+/*
+ * Build a 32-bit ADMA2 table: per 8-byte entry word0 = (len << 16) | attr,
+ * word1 = target address. When int_desc >= 0 that descriptor also carries
+ * the DMA-boundary interrupt attribute, so the model yields mid-chain
+ * (rescheduling on QEMU_CLOCK_VIRTUAL) instead of running to completion.
+ */
+static void build_adma_table(QTestState *qts, int int_desc)
+{
+ for (int i = 0; i < NDESC; i++) {
+ uint8_t attr = SDHC_ADMA_ATTR_VALID | SDHC_ADMA_ATTR_ACT_TRAN;
+ uint32_t addr = (uint32_t)(ADMA_BUF_ADDR + (uint64_t)i * BLK_LEN);
+
+ if (i == int_desc) {
+ attr |= SDHC_ADMA_ATTR_INT;
+ }
+ if (i == NDESC - 1) {
+ attr |= SDHC_ADMA_ATTR_END;
+ }
+ qtest_writel(qts, ADMA_TABLE_ADDR + (uint64_t)i * 8,
+ ((uint32_t)BLK_LEN << 16) | attr);
+ qtest_writel(qts, ADMA_TABLE_ADDR + (uint64_t)i * 8 + 4, addr);
+ }
+}
+
+/* Program the ADMA2 engine, descriptor pointer and transfer geometry. */
+static void program_adma_read(QTestState *qts)
+{
+ qtest_writeb(qts, SDHCI_BASE + SDHC_HOSTCTL, SDHC_CTRL_ADMA2_32);
+ qtest_writel(qts, SDHCI_BASE + SDHC_ADMASYSADDR,
(uint32_t)ADMA_TABLE_ADDR);
+ qtest_writel(qts, SDHCI_BASE + SDHC_ADMASYSADDR + 4,
+ (uint32_t)(ADMA_TABLE_ADDR >> 32));
+ qtest_writel(qts, SDHCI_BASE + SDHC_BLKSIZE, BLK_LEN | (NDESC << 16));
+ qtest_writew(qts, SDHCI_BASE + SDHC_TRNMOD,
+ SDHC_TRNS_DMA | SDHC_TRNS_BLK_CNT_EN |
+ SDHC_TRNS_READ | SDHC_TRNS_MULTI);
+ qtest_writel(qts, SDHCI_BASE + SDHC_ARGUMENT, 0); /* start block 0 */
+ qtest_writew(qts, SDHCI_BASE + SDHC_NORINTSTS, 0xffff);
+}
+
+/* CMD18 READ_MULTIPLE_BLOCK with data present -> kicks off ADMA. */
+static void kick_cmd18(QTestState *qts)
+{
+ qtest_writew(qts, SDHCI_BASE + SDHC_CMDREG,
+ (18 << 8) | SDHC_CMD_DATA_PRESENT | RESP_R48);
+}
+
+/* Start a card-carrying icicle machine and bring the card to "tran". */
+static QTestState *icicle_start(char **tmp)
+{
+ int fd;
+ GError *err = NULL;
+ QTestState *qts;
+
+ fd = g_file_open_tmp("icicle-sdhci-XXXXXX.raw", tmp, &err);
+ g_assert_no_error(err);
+ g_assert_cmpint(fd, >=, 0);
+ g_assert_cmpint(ftruncate(fd, 1 * 1024 * 1024), ==, 0);
+ close(fd);
+
+ qts = qtest_initf("-machine microchip-icicle-kit -accel qtest "
+ "-display none -drive if=sd,file=%s,format=raw", *tmp);
+ sd_bring_up_card(qts);
+ return qts;
+}
+
+/*
+ * Facet 1 -- pacing. A non-interrupt chain completes after some number of
+ * SDHC_TRANSFER_DELAY steps (several for a batched model, none for one that
+ * runs a no-interrupt chain to completion); the count is reported, not
+ * asserted -- only a clean Transfer Complete is required.
+ */
+static void test_adma_pacing(void)
+{
+ char *tmp = NULL;
+ QTestState *qts = icicle_start(&tmp);
+ uint16_t sts;
+ int steps = 0;
+
+ build_adma_table(qts, -1);
+ program_adma_read(qts);
+ kick_cmd18(qts);
+
+ while (!((sts = qtest_readw(qts, SDHCI_BASE + SDHC_NORINTSTS)) &
+ SDHC_NIS_TRSCMP)) {
+ qtest_clock_step(qts, SDHC_TRANSFER_DELAY);
+ steps++;
+ g_assert_cmpint(steps, <, 1000);
+ }
+
+ g_assert_cmphex(sts & SDHC_NIS_TRSCMP, ==, SDHC_NIS_TRSCMP);
+ g_assert_cmphex(sts & SDHC_NIS_ERR, ==, 0);
+
+ g_test_message("ADMA %d descriptors: %d clock steps to Transfer Complete",
+ NDESC, steps);
+
+ qtest_quit(qts);
+ unlink(tmp);
+ g_free(tmp);
+}
+
+/*
+ * Facet 2 -- MMIO coupling (regression for "Run ADMA independently of MMIO").
+ * With the clock frozen, a bare interrupt-status read must not resume the
+ * transfer left pending by the mid-chain interrupt descriptor: Transfer
+ * Complete must still be clear. A model that resumes ADMA from an MMIO read
+ * completes it on that bare read and fails the assertion.
+ */
+static void test_adma_mmio_coupling(void)
+{
+ char *tmp = NULL;
+ QTestState *qts = icicle_start(&tmp);
+ uint16_t sts;
+ int steps = 0;
+
+ build_adma_table(qts, INT_DESC);
+ program_adma_read(qts);
+ kick_cmd18(qts);
+
+ sts = qtest_readw(qts, SDHCI_BASE + SDHC_NORINTSTS);
+ g_assert_cmphex(sts & SDHC_NIS_TRSCMP, ==, 0);
+
+ while (!((sts = qtest_readw(qts, SDHCI_BASE + SDHC_NORINTSTS)) &
+ SDHC_NIS_TRSCMP)) {
+ qtest_clock_step(qts, SDHC_TRANSFER_DELAY);
+ steps++;
+ g_assert_cmpint(steps, <, 1000);
+ }
+ g_assert_cmphex(sts & SDHC_NIS_ERR, ==, 0);
+
+ g_test_message("ADMA coupling: %d clock steps to complete after bare read",
+ steps);
+
+ qtest_quit(qts);
+ unlink(tmp);
+ g_free(tmp);
+}
+
+int main(int argc, char **argv)
+{
+ g_test_init(&argc, &argv, NULL);
+ qtest_add_func("/microchip/icicle/sdhci/adma-pacing", test_adma_pacing);
+ qtest_add_func("/microchip/icicle/sdhci/adma-mmio-coupling",
+ test_adma_mmio_coupling);
+ return g_test_run();
+}
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 54e2cb0461..78e47ee024 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -283,6 +283,7 @@ qtests_riscv32 = \
(config_all_devices.has_key('CONFIG_SIFIVE_E_AON') ?
['sifive-e-aon-watchdog-test'] : [])
qtests_riscv64 = ['riscv-csr-test'] + \
+ (config_all_devices.has_key('CONFIG_MICROCHIP_PFSOC') ?
['cadence-sdhci-test'] : []) + \
(unpack_edk2_blobs ? ['bios-tables-test'] : [])
qos_test_ss = ss.source_set()
--
2.43.0