summaryrefslogtreecommitdiff
path: root/src/oracle/sdmmc_cases.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/oracle/sdmmc_cases.zig')
-rw-r--r--src/oracle/sdmmc_cases.zig567
1 files changed, 567 insertions, 0 deletions
diff --git a/src/oracle/sdmmc_cases.zig b/src/oracle/sdmmc_cases.zig
new file mode 100644
index 0000000..4b53aed
--- /dev/null
+++ b/src/oracle/sdmmc_cases.zig
@@ -0,0 +1,567 @@
+//! SDMMC's side of the differential test.
+//!
+//! Two windows, because this peripheral's state is not contiguous. The controller's own register
+//! block is at 0x50083000; its *host* clock generator - source mux, two-stage divider, sampling
+//! phase - is not in it at all, but in HP_SYS_CLKRST, where the P4 moved it. That is the same
+//! split I2C has (`i2c.clock_suite`), and for the same reason: a driver that programmed every
+//! register inside the block perfectly and the divider not at all would run the bus at the wrong
+//! frequency and pass every case in the first suite.
+//!
+//! **No case sends a command to the card.** The five `cmd_word_*` cases write the command register
+//! with `start_command` (bit 31) cleared, which is what makes them safe: bit 31 is the launch, and
+//! a word without it is inert. The C6 is in reset for the whole of a differ run - GPIO54 is never
+//! released - so a real CMD52 would sit out its response timeout and prove nothing. What is being
+//! compared is the encoding, and the encoding is entirely visible in the staged word.
+//!
+//! **Restore is the peripheral's own reset**, LP_AON_CLKRST bit 28, which is legitimate here and
+//! not merely convenient: this block is full of self-clearing and write-1-to-clear bits (the three
+//! reset bits in CTRL, every bit of RINTSTS, the IDMAC's software reset), and writing a snapshot
+//! back would trigger a reset rather than undo one. Nothing in this file restores through the code
+//! under test; the only Zig the harness runs between the two halves is `mmio`.
+
+const std = @import("std");
+const hal = @import("hal");
+const regs = @import("regs");
+const mmio = @import("mmio");
+const types = @import("differ_types.zig");
+
+extern fn oracle_sdmmc_bus_clock(enable: c_int) void;
+extern fn oracle_sdmmc_reset_register() void;
+extern fn oracle_sdmmc_set_host_clock_div(div: c_uint) void;
+extern fn oracle_sdmmc_select_clk_source_pll160m() void;
+extern fn oracle_sdmmc_init_phase_delay() void;
+extern fn oracle_sdmmc_set_card_clock_div(slot: c_uint, div: c_uint) void;
+extern fn oracle_sdmmc_enable_card_clock(slot: c_uint, enable: c_int) void;
+extern fn oracle_sdmmc_enable_card_clock_low_power(slot: c_uint, enable: c_int) void;
+extern fn oracle_sdmmc_reset_controller() void;
+extern fn oracle_sdmmc_reset_dma() void;
+extern fn oracle_sdmmc_reset_fifo() void;
+extern fn oracle_sdmmc_module_reset() void;
+extern fn oracle_sdmmc_set_card_width(slot: c_uint, width: c_uint) void;
+extern fn oracle_sdmmc_set_block_size(size: c_uint) void;
+extern fn oracle_sdmmc_set_data_transfer_len(len: c_uint) void;
+extern fn oracle_sdmmc_set_timeouts(data_cycles: c_uint, response_cycles: c_uint) void;
+extern fn oracle_sdmmc_set_fifo_threshold(rx: c_uint, tx: c_uint, msize: c_uint) void;
+extern fn oracle_sdmmc_configure_interrupts() void;
+extern fn oracle_sdmmc_init_dma() void;
+extern fn oracle_sdmmc_enable_dma(enable: c_int) void;
+extern fn oracle_sdmmc_set_desc_addr(addr: c_uint) void;
+extern fn oracle_sdmmc_enable_sdio_interrupt(slot: c_uint, enable: c_int) void;
+extern fn oracle_sdmmc_stage_command(
+ index: c_uint,
+ response_long: c_int,
+ response_expect: c_int,
+ check_crc: c_int,
+ data: c_int,
+ send_init: c_int,
+ wait_prvdata: c_int,
+ update_clk: c_int,
+ slot: c_uint,
+) void;
+extern fn oracle_sdmmc_version_id() c_uint;
+extern fn oracle_sdmmc_hw_config() c_uint;
+
+/// Printed by the harness's caller, so a run records which controller it was talking to. A version
+/// ID of 0 or 0xffffffff means the block is gated or absent and every result below is noise.
+pub fn versionId() u32 {
+ return oracle_sdmmc_version_id();
+}
+
+pub fn hwConfig() u32 {
+ return oracle_sdmmc_hw_config();
+}
+
+// Force the whole of `hal/sdmmc.zig` through the compiler for the *chip*.
+//
+// Zig analyses a function only when something references it, and this is the only build that
+// compiles that file for riscv32 at all - the plain application never mentions SDMMC, and the
+// host test root reaches only the pure encoding functions (it cannot reach the rest: reading the
+// `cycle` CSR does not assemble for x86). So without this list, `cmd53Read`, `cardInit` and the
+// whole transfer path would be text that has never been type-checked against the target, which is
+// a bad thing to discover on a board.
+//
+// A `-Doracle` build failing here is the intended behaviour: it means the driver does not
+// compile, and it says so before anything is flashed.
+comptime {
+ _ = &hal.sdmmc.init;
+ _ = &hal.sdmmc.cardInit;
+ _ = &hal.sdmmc.cmd52Read;
+ _ = &hal.sdmmc.cmd52Write;
+ _ = &hal.sdmmc.cmd53Read;
+ _ = &hal.sdmmc.cmd53Write;
+ _ = &hal.sdmmc.slaveInterruptPending;
+ _ = &hal.sdmmc.clearSlaveInterrupt;
+ _ = &hal.sdmmc.setSlaveInterruptEnabled;
+ _ = &hal.sdmmc.rca;
+ _ = &hal.sdmmc.configurePins;
+ _ = &hal.sdmmc.setBusClock;
+ _ = &hal.sdmmc.dividersFor;
+ _ = &hal.sdmmc.cmd52Arg;
+ _ = &hal.sdmmc.cmd53Arg;
+ _ = hal.sdmmc.interrupt_source;
+ _ = hal.sdmmc.bounce_len;
+ _ = hal.sdmmc.c6_pins;
+}
+
+/// The slot under test. Slot 1 is where the ESP32-C6 is; slot 0's pads are the P4's own flash
+/// interface on this board and are never touched.
+const slot: u1 = 1;
+
+const cmd_reg = mmio.Reg.atAddress(@intCast(regs.SDHOST_CMD_REG));
+
+/// Stage the word our HAL would send, with the launch bit removed. `hal.sdmmc.commandWord` is the
+/// code under test; the store is one line and is not.
+fn stage(c: hal.sdmmc.Command) void {
+ cmd_reg.writeRaw(hal.sdmmc.commandWord(c) & ~(@as(u32, 1) << 31));
+}
+
+// -------------------------------------------------------------------------- the register block
+
+pub const suite: types.Suite = .{
+ .descriptor = .{
+ .name = "sdmmc",
+ .base = @intCast(regs.SDHOST_CTRL_REG), // offset 0 of the block
+ // 0x000 through ENSHIFT at +0x110. The window deliberately stops short of BUFFIFO at
+ // +0x200: that is the data FIFO, and a snapshot loop that read it would pop received
+ // words - the same hazard `UART_FIFO_REG` poses at offset 0 of every UART. The three
+ // registers above it (CLK_EDGE_SEL, RAW_INTS, DLL_CLK_CONF at +0x800) belong to the
+ // high-speed delay-line path this driver does not use.
+ .words = 69,
+ .volatile_words = &.{
+ (0x40 - 0x00) / 4, // MINTSTS - the C6 can raise its SDIO interrupt at any moment
+ (0x44 - 0x00) / 4, // RINTSTS - likewise, and write-1-to-clear
+ (0x48 - 0x00) / 4, // STATUS - FIFO count, FSM state, live DAT levels
+ (0x50 - 0x00) / 4, // CDETECT - a live input
+ (0x54 - 0x00) / 4, // WRTPRT - a live input
+ (0x5c - 0x00) / 4, // TCBCNT - transferred card byte count
+ (0x60 - 0x00) / 4, // TBBCNT - transferred host byte count
+ (0x8c - 0x00) / 4, // IDSTS - IDMAC status, write-1-to-clear
+ (0x94 - 0x00) / 4, // DSCADDR - the IDMAC's current descriptor pointer
+ (0x98 - 0x00) / 4, // BUFADDR - the IDMAC's current buffer pointer
+ },
+ // Unlike most of this chip, SDMMC powers up with its bus clock *off*
+ // (HP_SYS_CLKRST SOC_CLK_CTRL1 REG_SDMMC_SYS_CLK_EN, default 0), so this check is the one
+ // that catches a setup that silently did not happen: a gated block returns the last value
+ // latched, not zeros, and two such snapshots compare equal while describing nothing.
+ .clock = .{
+ .reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL1_REG),
+ .bit = @intCast(regs.HP_SYS_CLKRST_REG_SDMMC_SYS_CLK_EN_S),
+ },
+ // LP_AON_CLKRST.hp_sdmmc_emac_rst_ctrl.rst_en_sdmmc - `sdmmc_ll.h:158-163`. Not in
+ // HP_SYS_CLKRST with almost every other peripheral's reset, which is the single most
+ // surprising fact about this block's clock and reset wiring.
+ .restore = .{ .reset_bit = .{
+ .reg = @intCast(regs.LP_CLKRST_HP_SDMMC_EMAC_RST_CTRL_REG),
+ .bit = @intCast(regs.LP_CLKRST_RST_EN_SDMMC_S),
+ } },
+ },
+ .cases = &.{
+ // --- resets. Each of the three bits is self-clearing, so what these compare is mostly
+ // that the *other* bits of CTRL come out the same: a reset function that wrote bit 5
+ // (dma_enable) instead of bit 2 (dma_reset) would leave a trace, and that is exactly the
+ // kind of slip the two undocumented CTRL bits invite.
+ .{ .name = "reset_controller", .idf = idfResetCtl, .ours = ourResetCtl },
+ .{ .name = "reset_fifo", .idf = idfResetFifo, .ours = ourResetFifo },
+ .{ .name = "reset_dma", .idf = idfResetDma, .ours = ourResetDma },
+ .{ .name = "module_reset", .idf = idfModuleReset, .ours = ourModuleReset },
+ // --- the card clock: CLKDIV, CLKSRC, CLKENA. Divider 0 is bypass (40 MHz through the
+ // host divider alone); divider 20 is the 400 kHz probing setting.
+ .{ .name = "card_clock_div", .arg = 0, .idf = idfCardDiv0, .ours = ourCardDiv0 },
+ .{ .name = "card_clock_div", .arg = 20, .idf = idfCardDiv20, .ours = ourCardDiv20 },
+ .{ .name = "card_clock_enable", .arg = 1, .idf = idfCclkOn, .ours = ourCclkOn },
+ .{ .name = "card_clock_low_power", .arg = 0, .idf = idfLpOff, .ours = ourLpOff },
+ .{ .name = "card_clock_low_power", .arg = 1, .idf = idfLpOn, .ours = ourLpOn },
+ // --- bus width. The measured working dump has ctype=0x00000002, i.e. bit 1: slot 1 in
+ // 4-bit mode, which is what `bus_width(4)` must produce and nothing else.
+ .{ .name = "bus_width", .arg = 4, .idf = idfWidth4, .ours = ourWidth4 },
+ .{ .name = "bus_width", .arg = 1, .idf = idfWidth1, .ours = ourWidth1 },
+ // --- transfer geometry.
+ .{ .name = "block_size", .arg = 512, .idf = idfBlk512, .ours = ourBlk512 },
+ .{ .name = "block_size", .arg = 4, .idf = idfBlk4, .ours = ourBlk4 },
+ .{ .name = "timeouts", .idf = idfTimeouts, .ours = ourTimeouts },
+ // A deliberately non-default watermark set, so the case is not "both wrote the reset
+ // value". ESP-IDF has no LL function for FIFOTH at all and never writes the register on
+ // any target, so the reference here goes through IDF's `SDMMC.fifoth` bitfields instead -
+ // which is still IDF's definition of where those three fields sit.
+ .{ .name = "fifo_threshold", .arg = 255, .idf = idfFifoth, .ours = ourFifoth },
+ .{ .name = "fifo_threshold_default", .arg = 511, .idf = idfFifothDefault, .ours = ourFifothDefault },
+ // --- interrupts and DMA.
+ .{ .name = "configure_interrupts", .idf = idfIntrs, .ours = ourIntrs },
+ .{ .name = "sdio_interrupt", .arg = 1, .idf = idfSdioIntOn, .ours = ourSdioIntOn },
+ .{ .name = "init_dma", .idf = idfInitDma, .ours = ourInitDma },
+ .{ .name = "desc_addr", .idf = idfDescAddr, .ours = ourDescAddr },
+ // --- command-word encodings. The five commands `cardInit` sends, plus both directions of
+ // CMD53 and the clock update command that is not a command at all.
+ .{ .name = "cmd_word_cmd0", .arg = 0, .idf = idfCmd0, .ours = ourCmd0 },
+ .{ .name = "cmd_word_cmd5", .arg = 5, .idf = idfCmd5, .ours = ourCmd5 },
+ .{ .name = "cmd_word_cmd3", .arg = 3, .idf = idfCmd3, .ours = ourCmd3 },
+ .{ .name = "cmd_word_cmd7", .arg = 7, .idf = idfCmd7, .ours = ourCmd7 },
+ .{ .name = "cmd_word_cmd52_read", .arg = 52, .idf = idfCmd52R, .ours = ourCmd52R },
+ .{ .name = "cmd_word_cmd52_write", .arg = 52, .idf = idfCmd52W, .ours = ourCmd52W },
+ .{ .name = "cmd_word_cmd53_read", .arg = 53, .idf = idfCmd53R, .ours = ourCmd53R },
+ .{ .name = "cmd_word_cmd53_write", .arg = 53, .idf = idfCmd53W, .ours = ourCmd53W },
+ .{ .name = "cmd_word_clock_update", .idf = idfCmdClk, .ours = ourCmdClk },
+ // --- the peripheral reset itself, which is in LP_AON_CLKRST and observable here only by
+ // its effect: configure the block distinctively through IDF's LL on both sides, then let
+ // each implementation reset it. A `resetPeripheral(.sdmmc)` that wrote the wrong bit -
+ // there is no HP_SYS_CLKRST reset for SDMMC, so writing one is the obvious mistake - would
+ // leave the configuration standing.
+ .{ .name = "peripheral_reset", .idf = idfPeriphReset, .ours = ourPeriphReset },
+ },
+ .setup = setup,
+};
+
+/// Bring the block up far enough that its registers are live, and settle the HAL's idea of which
+/// slot it is driving.
+///
+/// The clock and the reset go through ESP-IDF's LL, not ours: setup runs once, before any case,
+/// and a setup written with the code under test would hide a broken `clkrst.init(.sdmmc)` behind
+/// its own success. `hal.sdmmc.init` runs afterwards for a different reason - it is the only way
+/// to tell the HAL that this is slot 1, and running it here means a bring-up that hangs shows up
+/// as a stalled suite rather than as a wrong register somewhere later. Its result is discarded:
+/// every case restores the block by resetting it, so nothing init leaves behind is load-bearing,
+/// and a card that never answers must not stop the register comparison from running.
+fn setup() void {
+ oracle_sdmmc_bus_clock(1);
+ oracle_sdmmc_reset_register();
+ hal.sdmmc.init(.{ .slot = slot, .width = .four, .khz = 40_000 }) catch {};
+}
+
+fn idfResetCtl() void {
+ oracle_sdmmc_reset_controller();
+}
+fn ourResetCtl() void {
+ mmio.Reg.atAddress(@intCast(regs.SDHOST_CTRL_REG)).modify(.{
+ mmio.Field.of(regs.SDHOST_CONTROLLER_RESET_S, regs.SDHOST_CONTROLLER_RESET_V).is(1),
+ });
+}
+fn idfResetFifo() void {
+ oracle_sdmmc_reset_fifo();
+}
+fn ourResetFifo() void {
+ mmio.Reg.atAddress(@intCast(regs.SDHOST_CTRL_REG)).modify(.{
+ mmio.Field.of(regs.SDHOST_FIFO_RESET_S, regs.SDHOST_FIFO_RESET_V).is(1),
+ });
+}
+fn idfResetDma() void {
+ oracle_sdmmc_reset_dma();
+}
+fn ourResetDma() void {
+ mmio.Reg.atAddress(@intCast(regs.SDHOST_CTRL_REG)).modify(.{
+ mmio.Field.of(regs.SDHOST_DMA_RESET_S, regs.SDHOST_DMA_RESET_V).is(1),
+ });
+}
+fn idfModuleReset() void {
+ oracle_sdmmc_module_reset();
+}
+fn ourModuleReset() void {
+ hal.sdmmc.resetController() catch {};
+}
+
+fn idfCardDiv0() void {
+ oracle_sdmmc_set_card_clock_div(slot, 0);
+}
+fn ourCardDiv0() void {
+ hal.sdmmc.setCardClockDiv(0);
+}
+fn idfCardDiv20() void {
+ oracle_sdmmc_set_card_clock_div(slot, 20);
+}
+fn ourCardDiv20() void {
+ hal.sdmmc.setCardClockDiv(20);
+}
+
+fn idfCclkOn() void {
+ oracle_sdmmc_enable_card_clock(slot, 1);
+}
+fn ourCclkOn() void {
+ hal.sdmmc.setCardClockEnabled(true);
+}
+fn idfLpOff() void {
+ oracle_sdmmc_enable_card_clock_low_power(slot, 0);
+}
+fn ourLpOff() void {
+ hal.sdmmc.setCardClockLowPower(false);
+}
+fn idfLpOn() void {
+ oracle_sdmmc_enable_card_clock_low_power(slot, 1);
+}
+fn ourLpOn() void {
+ hal.sdmmc.setCardClockLowPower(true);
+}
+
+fn idfWidth4() void {
+ oracle_sdmmc_set_card_width(slot, 4);
+}
+fn ourWidth4() void {
+ hal.sdmmc.setBusWidth(.four);
+}
+fn idfWidth1() void {
+ oracle_sdmmc_set_card_width(slot, 1);
+}
+fn ourWidth1() void {
+ hal.sdmmc.setBusWidth(.one);
+}
+
+fn idfBlk512() void {
+ oracle_sdmmc_set_block_size(512);
+ oracle_sdmmc_set_data_transfer_len(512);
+}
+fn ourBlk512() void {
+ hal.sdmmc.setBlockSize(512);
+ hal.sdmmc.setDataTransferLen(512);
+}
+fn idfBlk4() void {
+ // The geometry the measured working dump was taken at: blksiz=4 bytcnt=4, the four-byte
+ // register read ESP-Hosted does to find out how much the slave has queued.
+ oracle_sdmmc_set_block_size(4);
+ oracle_sdmmc_set_data_transfer_len(4);
+}
+fn ourBlk4() void {
+ hal.sdmmc.setBlockSize(4);
+ hal.sdmmc.setDataTransferLen(4);
+}
+
+fn idfTimeouts() void {
+ // 100 ms of card clocks at 40 MHz, and the maximum response timeout - `sd_host_sdmmc.c:531-535`.
+ oracle_sdmmc_set_timeouts(100 * 40_000, 255);
+}
+fn ourTimeouts() void {
+ hal.sdmmc.setTimeouts(100 * 40_000, 255);
+}
+
+fn idfFifoth() void {
+ oracle_sdmmc_set_fifo_threshold(255, 8, 2);
+}
+fn ourFifoth() void {
+ hal.sdmmc.setFifoThreshold(255, 8, 2);
+}
+fn idfFifothDefault() void {
+ oracle_sdmmc_set_fifo_threshold(511, 0, 0);
+}
+fn ourFifothDefault() void {
+ hal.sdmmc.setFifoThreshold(
+ hal.sdmmc.default_rx_watermark,
+ hal.sdmmc.default_tx_watermark,
+ hal.sdmmc.default_dma_msize,
+ );
+}
+
+fn idfIntrs() void {
+ oracle_sdmmc_configure_interrupts();
+}
+fn ourIntrs() void {
+ hal.sdmmc.configureInterrupts();
+}
+fn idfSdioIntOn() void {
+ oracle_sdmmc_enable_sdio_interrupt(slot, 1);
+}
+fn ourSdioIntOn() void {
+ hal.sdmmc.setSlaveInterruptEnabled(true);
+}
+
+fn idfInitDma() void {
+ oracle_sdmmc_init_dma();
+ oracle_sdmmc_enable_dma(1);
+}
+fn ourInitDma() void {
+ hal.sdmmc.initDma();
+ hal.sdmmc.setDmaEnabled(true);
+}
+
+/// An address in L2MEM with the low bits set to something a bug would round away: DBADDR ignores
+/// bits [1:0] internally but stores what is written.
+const test_desc_addr: u32 = 0x4ff1_0140;
+
+fn idfDescAddr() void {
+ oracle_sdmmc_set_desc_addr(test_desc_addr);
+}
+fn ourDescAddr() void {
+ hal.sdmmc.setDescriptorAddr(test_desc_addr);
+}
+
+// The command words. Each pair is the same command expressed twice: once through ESP-IDF's
+// `sdmmc_hw_cmd_t` bitfields, once through this project's `commandWord`.
+
+fn idfCmd0() void {
+ oracle_sdmmc_stage_command(0, 0, 0, 0, 0, 1, 0, 0, slot);
+}
+fn ourCmd0() void {
+ stage(.{ .index = 0, .send_init = true, .wait_prvdata = false, .slot = slot });
+}
+fn idfCmd5() void {
+ oracle_sdmmc_stage_command(5, 0, 1, 0, 0, 0, 1, 0, slot);
+}
+fn ourCmd5() void {
+ stage(.{ .index = 5, .response = .short, .check_crc = false, .slot = slot });
+}
+fn idfCmd3() void {
+ oracle_sdmmc_stage_command(3, 0, 1, 1, 0, 0, 1, 0, slot);
+}
+fn ourCmd3() void {
+ stage(.{ .index = 3, .response = .short, .check_crc = true, .slot = slot });
+}
+fn idfCmd7() void {
+ oracle_sdmmc_stage_command(7, 0, 1, 1, 0, 0, 1, 0, slot);
+}
+fn ourCmd7() void {
+ stage(.{ .index = 7, .response = .short, .check_crc = true, .slot = slot });
+}
+fn idfCmd52R() void {
+ oracle_sdmmc_stage_command(52, 0, 1, 1, 0, 0, 1, 0, slot);
+}
+fn ourCmd52R() void {
+ stage(.{ .index = 52, .response = .short, .check_crc = true, .slot = slot });
+}
+fn idfCmd52W() void {
+ // CMD52 carries its payload in the argument, not in a data phase, so the word is identical to
+ // the read one. Kept as its own case because that is a claim worth checking rather than
+ // assuming: an implementation that set `rw` for a write would fail here and nowhere else.
+ oracle_sdmmc_stage_command(52, 0, 1, 1, 0, 0, 1, 0, slot);
+}
+fn ourCmd52W() void {
+ stage(.{ .index = 52, .response = .short, .check_crc = true, .slot = slot });
+}
+fn idfCmd53R() void {
+ oracle_sdmmc_stage_command(53, 0, 1, 1, 1, 0, 1, 0, slot);
+}
+fn ourCmd53R() void {
+ stage(.{ .index = 53, .response = .short, .check_crc = true, .data = .read, .slot = slot });
+}
+fn idfCmd53W() void {
+ oracle_sdmmc_stage_command(53, 0, 1, 1, 2, 0, 1, 0, slot);
+}
+fn ourCmd53W() void {
+ stage(.{ .index = 53, .response = .short, .check_crc = true, .data = .write, .slot = slot });
+}
+fn idfCmdClk() void {
+ oracle_sdmmc_stage_command(0, 0, 0, 0, 0, 0, 1, 1, slot);
+}
+fn ourCmdClk() void {
+ stage(.{ .index = 0, .update_clock = true, .slot = slot });
+}
+
+/// A configuration distinctive enough that failing to clear it is visible in three registers.
+fn configureDistinctively() void {
+ oracle_sdmmc_set_card_width(slot, 4);
+ oracle_sdmmc_set_block_size(4);
+ oracle_sdmmc_set_fifo_threshold(255, 8, 2);
+}
+
+fn idfPeriphReset() void {
+ configureDistinctively();
+ oracle_sdmmc_reset_register();
+}
+fn ourPeriphReset() void {
+ configureDistinctively();
+ hal.clkrst.resetPeripheral(.sdmmc);
+}
+
+// ------------------------------------------------------------------- the host clock generator
+
+/// The other half of "set the bus to 40 MHz", which is not in the SDMMC block.
+///
+/// `HP_SYS_CLKRST.peri_clk_ctrl01` holds the source mux and the gate, `peri_clk_ctrl02` the
+/// three-edge divider and the driving/sampling phase clocks (`sdmmc_ll.h:212-315`). At 40 MHz the
+/// host divider is 4 and the card divider is 0, so *all* of the division happens here: an
+/// implementation that wrote CLKDIV correctly and this register not at all would clock the C6 at
+/// 160 MHz, which is four times the part's limit and would fail as a wiring problem.
+pub const clock_suite: types.Suite = .{
+ .descriptor = .{
+ .name = "sdmmc_clk",
+ .base = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL1_REG - 0x18), // block base
+ // 0x00 through PERI_CLK_CTRL03 at +0x3c: SOC_CLK_CTRL0..3 (the bus-clock gates) and
+ // PERI_CLK_CTRL00..03 (the SDIO clock generator).
+ .words = 16,
+ // No clock check: HP_SYS_CLKRST is the block that holds every other block's gate and has
+ // none of its own, and one of the cases below deliberately turns SDMMC's off.
+ .restore = .{ .configure = restoreClocks },
+ },
+ .cases = &.{
+ // Disable first, so "enable" is not a no-op against a restored state that already has it
+ // on - the shape clkrst_cases.zig arrived at for the same reason.
+ .{ .name = "bus_clock", .arg = 0, .idf = idfBusClkOff, .ours = ourBusClkOff },
+ .{ .name = "host_clock_div", .arg = 4, .idf = idfHostDiv4, .ours = ourHostDiv4 },
+ .{ .name = "host_clock_div", .arg = 8, .idf = idfHostDiv8, .ours = ourHostDiv8 },
+ .{ .name = "host_clock_div", .arg = 10, .idf = idfHostDiv10, .ours = ourHostDiv10 },
+ .{ .name = "select_clk_source", .idf = idfSelectSrc, .ours = ourSelectSrc },
+ .{ .name = "init_phase_delay", .idf = idfPhase, .ours = ourPhase },
+ // Last, so the block is left clocked whichever side ran last: every suite after this one
+ // that touches SDMMC depends on it.
+ .{ .name = "bus_clock", .arg = 1, .idf = idfBusClkOn, .ours = ourBusClkOn },
+ },
+};
+
+const soc_clk_ctrl1 = mmio.Reg.atAddress(@intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL1_REG));
+const peri01 = mmio.Reg.atAddress(@intCast(regs.HP_SYS_CLKRST_PERI_CLK_CTRL01_REG));
+const peri02 = mmio.Reg.atAddress(@intCast(regs.HP_SYS_CLKRST_PERI_CLK_CTRL02_REG));
+
+/// Every SDIO field of the three registers this suite's cases touch, back to its reset value -
+/// and nothing else, because these words also hold the gates and clock muxes of peripherals that
+/// have nothing to do with SDMMC (MIPI DSI's D-PHY source select is bits 30-31 of PERI_CLK_CTRL02).
+/// Built from register macros only; nothing here calls the code under test.
+fn restoreClocks() void {
+ soc_clk_ctrl1.modify(.{
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDMMC_SYS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDMMC_SYS_CLK_EN_V).is(1),
+ });
+ peri01.modify(.{
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_HS_MODE_S, regs.HP_SYS_CLKRST_REG_SDIO_HS_MODE_V).is(0),
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_SRC_SEL_V).is(0),
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EN_V).is(0),
+ });
+ peri02.modify(.{
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_L_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_L_V).is(0),
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_H_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_H_V).is(0),
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_N_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_N_V).is(0),
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_SLF_CLK_EDGE_SEL_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_SLF_CLK_EDGE_SEL_V).is(0),
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_DRV_CLK_EDGE_SEL_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_DRV_CLK_EDGE_SEL_V).is(0),
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_SAM_CLK_EDGE_SEL_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_SAM_CLK_EDGE_SEL_V).is(0),
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_SLF_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_SLF_CLK_EN_V).is(0),
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_DRV_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_DRV_CLK_EN_V).is(0),
+ mmio.Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_SAM_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_SAM_CLK_EN_V).is(0),
+ });
+}
+
+fn idfBusClkOff() void {
+ oracle_sdmmc_bus_clock(0);
+}
+fn ourBusClkOff() void {
+ hal.clkrst.setClockEnabled(.sdmmc, false);
+}
+fn idfBusClkOn() void {
+ oracle_sdmmc_bus_clock(1);
+}
+fn ourBusClkOn() void {
+ hal.clkrst.setClockEnabled(.sdmmc, true);
+}
+
+fn idfHostDiv4() void {
+ oracle_sdmmc_set_host_clock_div(4);
+}
+fn ourHostDiv4() void {
+ hal.sdmmc.setHostClockDiv(4);
+}
+fn idfHostDiv8() void {
+ oracle_sdmmc_set_host_clock_div(8);
+}
+fn ourHostDiv8() void {
+ hal.sdmmc.setHostClockDiv(8);
+}
+fn idfHostDiv10() void {
+ oracle_sdmmc_set_host_clock_div(10);
+}
+fn ourHostDiv10() void {
+ hal.sdmmc.setHostClockDiv(10);
+}
+fn idfSelectSrc() void {
+ oracle_sdmmc_select_clk_source_pll160m();
+}
+fn ourSelectSrc() void {
+ hal.sdmmc.selectPll160m();
+}
+fn idfPhase() void {
+ oracle_sdmmc_init_phase_delay();
+}
+fn ourPhase() void {
+ hal.sdmmc.initPhaseDelay();
+}