summaryrefslogtreecommitdiff
path: root/src/oracle/sdmmc_cases.zig
blob: 4b53aed764dc9ece25bd3df72ddac5d4e68a4a27 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
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();
}