//! The SDMMC host controller, driven as an **SDIO host**. //! //! There is no SD card on this board. Slot 1 of the P4's SDMMC controller goes to an ESP32-C6 //! running ESP-Hosted coprocessor firmware, which presents itself as a 4-bit SDIO device: CLK 18, //! CMD 19, D0-D3 = 14/15/16/17. So this file implements CMD0/CMD5/CMD3/CMD7 and then CMD52/CMD53, //! and nothing above them. SD memory cards, SPI mode, CSD/CID decoding and block devices are //! deliberately absent - they are a different problem that happens to share a peripheral. //! //! The controller is a Synopsys DesignWare mobile-storage host. Three of its properties decide the //! shape of everything below. //! //! **The card clock is not the register clock.** CLKDIV, CLKSRC and CLKENA are written on the bus //! side and do not reach the card-interface unit until a *clock update command* is issued: a write //! to the CMD register with `update_clk_reg` and `start_command` set, which sends nothing to the //! card (`sdmmc_reg.h:440-456`, and ESP-IDF's `sd_host_slot_clock_update_command`, //! `sd_host_sdmmc.c:896-912`). A driver that programmes a divider and moves on has changed //! nothing. Three such commands are needed to change frequency safely - clock off, reprogramme, //! clock on - and that is what `setBusClock` does. //! //! **The command register is a single word, and `start_command` is bit 31 of it.** Every attribute //! of a command - index, whether a response is expected, whether its CRC is checked, whether data //! follows and in which direction - is a field of the same word, and writing that word with bit 31 //! set launches the command. So the interesting part of "send CMD52" is an encoding, not a //! sequence, and `commandWord` is a pure function of the request. It is host-tested, and the //! oracle compares the words it produces against words built through ESP-IDF's own //! `sdmmc_hw_cmd_t` bitfields. //! //! **Data moves by internal DMA over descriptors in memory, and the P4 caches that memory.** //! `soc_caps.h:185` sets SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE, so L2MEM - where every static in this //! image lives - is reached by the CPU through the L1 data cache while the IDMAC reaches it //! directly. See the "Cache" section below for the resolution; it is the one place in this file //! where the right answer is not visible in any register header. //! //! Nothing here has been run on hardware by the author of this file. What is claimed is that the //! register arithmetic and the command encodings match ESP-IDF's at the cited lines, that //! `src/oracle/sdmmc_cases.zig` compares the two on the die, and that the configuration `init` //! leaves behind reproduces a dump taken from a working ESP-IDF image on this board. const std = @import("std"); const regs = @import("regs"); const mmio = @import("mmio"); const gpio = @import("gpio.zig"); const clkrst = @import("clkrst.zig"); const intr = @import("intr.zig"); const Reg = mmio.Reg; const Field = mmio.Field; pub const Error = error{ Timeout, CrcError, ResponseError, NotSupported, Busy }; // ------------------------------------------------------------------------------- registers // // One instance, at DR_REG_SDHOST_BASE = DR_REG_SDMMC_BASE = 0x50083000 (`reg_base.h:44`, `:204`, // and `esp32p4.peripherals.ld:41` agrees). The macros are spelled SDHOST_*, the peripheral is // spelled SDMMC, and both names are ESP-IDF's. const ctrl = Reg.at(regs.SDHOST_CTRL_REG); const clkdiv = Reg.at(regs.SDHOST_CLKDIV_REG); const clksrc = Reg.at(regs.SDHOST_CLKSRC_REG); const clkena = Reg.at(regs.SDHOST_CLKENA_REG); const tmout = Reg.at(regs.SDHOST_TMOUT_REG); const ctype = Reg.at(regs.SDHOST_CTYPE_REG); const blksiz = Reg.at(regs.SDHOST_BLKSIZ_REG); const bytcnt = Reg.at(regs.SDHOST_BYTCNT_REG); const intmask = Reg.at(regs.SDHOST_INTMASK_REG); const cmdarg = Reg.at(regs.SDHOST_CMDARG_REG); const cmd = Reg.at(regs.SDHOST_CMD_REG); const resp0 = Reg.at(regs.SDHOST_RESP0_REG); const rintsts = Reg.at(regs.SDHOST_RINTSTS_REG); /// The *masked* status: RINTSTS gated by INTMASK, and the only word the controller's interrupt /// output looks at. ESP-IDF's `sdmmc_ll_get_intr_status` reads this one and not RINTSTS /// (`sdmmc_ll.h:841-844`), which is exactly why INTMASK decides what reaches the CLIC while /// RINTSTS stays readable for the polling path. const mintsts = Reg.at(regs.SDHOST_MINTSTS_REG); const status = Reg.at(regs.SDHOST_STATUS_REG); const fifoth = Reg.at(regs.SDHOST_FIFOTH_REG); const bmod = Reg.at(regs.SDHOST_BMOD_REG); const pldmnd = Reg.at(regs.SDHOST_PLDMND_REG); const dbaddr = Reg.at(regs.SDHOST_DBADDR_REG); const idsts = Reg.at(regs.SDHOST_IDSTS_REG); const idinten = Reg.at(regs.SDHOST_IDINTEN_REG); // CTRL fields. Two of them - `dma_enable` at bit 5 and `use_internal_dma` at bit 25 - have no // `_S`/`_V` macro pair in `sdmmc_reg.h` at all: that header documents CTRL as bits 0,1,2,4,6..11 // and stops. They are real, they are in the measured working dump (`ctrl=0x02000030`), and // `sdmmc_struct.h:76` and `:135` name them at exactly those positions. This is the same situation // as the IO MUX pull bits in `hal/gpio.zig`, and the same remedy: `Field.bit` with the struct // header cited, because the struct header is ESP-IDF's definition of the layout even where the // macro header is incomplete. const controller_reset = Field.of(regs.SDHOST_CONTROLLER_RESET_S, regs.SDHOST_CONTROLLER_RESET_V); const fifo_reset = Field.of(regs.SDHOST_FIFO_RESET_S, regs.SDHOST_FIFO_RESET_V); const dma_reset = Field.of(regs.SDHOST_DMA_RESET_S, regs.SDHOST_DMA_RESET_V); const int_enable = Field.of(regs.SDHOST_INT_ENABLE_S, regs.SDHOST_INT_ENABLE_V); /// `sdmmc_struct.h:76` - `uint32_t dma_enable:1;` immediately after `int_enable:1` at bit 4. const dma_enable = Field.bit(5); /// `sdmmc_struct.h:135` - after `reserved2:4`, `card_voltage_a:4`, `card_voltage_b:4` and /// `enable_od_pullup:1`, i.e. bit 25. `sdmmc_ll_enable_dma` (`sdmmc_ll.h:812-818`) is the only /// writer, and the working dump's `ctrl=0x02000030` has exactly this bit plus 4 and 5. const use_internal_dma = Field.bit(25); const clk_divider0 = Field.of(regs.SDHOST_CLK_DIVIDER0_S, regs.SDHOST_CLK_DIVIDER0_V); const clk_divider1 = Field.of(regs.SDHOST_CLK_DIVIDER1_S, regs.SDHOST_CLK_DIVIDER1_V); // CLKSRC is documented as one 4-bit field, two bits per card ("bit[1:0] are assigned for card 0, // bit[3:2] are assigned for card 1", `sdmmc_reg.h:166-179`). `sdmmc_struct.h:191-192` splits it // into `card0:2` and `card1:2`, which is the shape a driver wants; there are no macros for the // halves, so the two sub-fields are spelled out with that citation. const clksrc_card0 = Field.of(0, 0x3); const clksrc_card1 = Field.of(2, 0x3); const cclk_enable = Field.of(regs.SDHOST_CCLK_ENABLE_S, regs.SDHOST_CCLK_ENABLE_V); const lp_enable = Field.of(regs.SDHOST_LP_ENABLE_S, regs.SDHOST_LP_ENABLE_V); const response_timeout = Field.of(regs.SDHOST_RESPONSE_TIMEOUT_S, regs.SDHOST_RESPONSE_TIMEOUT_V); const data_timeout = Field.of(regs.SDHOST_DATA_TIMEOUT_S, regs.SDHOST_DATA_TIMEOUT_V); const card_width4 = Field.of(regs.SDHOST_CARD_WIDTH4_S, regs.SDHOST_CARD_WIDTH4_V); const card_width8 = Field.of(regs.SDHOST_CARD_WIDTH8_S, regs.SDHOST_CARD_WIDTH8_V); const block_size = Field.of(regs.SDHOST_BLOCK_SIZE_S, regs.SDHOST_BLOCK_SIZE_V); const byte_count = Field.of(regs.SDHOST_BYTE_COUNT_S, regs.SDHOST_BYTE_COUNT_V); const int_mask = Field.of(regs.SDHOST_INT_MASK_S, regs.SDHOST_INT_MASK_V); const sdio_int_mask = Field.of(regs.SDHOST_SDIO_INT_MASK_S, regs.SDHOST_SDIO_INT_MASK_V); const data_busy = Field.of(regs.SDHOST_DATA_BUSY_S, regs.SDHOST_DATA_BUSY_V); const tx_wmark = Field.of(regs.SDHOST_TX_WMARK_S, regs.SDHOST_TX_WMARK_V); const rx_wmark = Field.of(regs.SDHOST_RX_WMARK_S, regs.SDHOST_RX_WMARK_V); const dma_msize = Field.of(regs.SDHOST_DMA_MULTIPLE_TRANSACTION_SIZE_S, regs.SDHOST_DMA_MULTIPLE_TRANSACTION_SIZE_V); const bmod_swr = Field.of(regs.SDHOST_BMOD_SWR_S, regs.SDHOST_BMOD_SWR_V); const bmod_fb = Field.of(regs.SDHOST_BMOD_FB_S, regs.SDHOST_BMOD_FB_V); const bmod_de = Field.of(regs.SDHOST_BMOD_DE_S, regs.SDHOST_BMOD_DE_V); const idinten_ti = Field.of(regs.SDHOST_IDINTEN_TI_S, regs.SDHOST_IDINTEN_TI_V); const idinten_ri = Field.of(regs.SDHOST_IDINTEN_RI_S, regs.SDHOST_IDINTEN_RI_V); const idinten_ni = Field.of(regs.SDHOST_IDINTEN_NI_S, regs.SDHOST_IDINTEN_NI_V); // The host-side clock generator, which is *not* in the SDMMC block: the P4 moved it into // HP_SYS_CLKRST, and it is the first of two divider stages (this one, then CLKDIV inside the // controller). `sdmmc_ll.h:227-228` for the source mux and gate, `:244-258` for the divider, // `:305-315` for the sampling/driving phase clocks. const peri_clk_ctrl01 = Reg.at(regs.HP_SYS_CLKRST_PERI_CLK_CTRL01_REG); const peri_clk_ctrl02 = Reg.at(regs.HP_SYS_CLKRST_PERI_CLK_CTRL02_REG); const sdio_hs_mode = Field.of(regs.HP_SYS_CLKRST_REG_SDIO_HS_MODE_S, regs.HP_SYS_CLKRST_REG_SDIO_HS_MODE_V); const sdio_ls_clk_src_sel = Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_SRC_SEL_V); const sdio_ls_clk_en = Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EN_V); const sdio_ls_clk_edge_cfg_update = Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_CFG_UPDATE_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_CFG_UPDATE_V); const sdio_ls_clk_edge_l = Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_L_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_L_V); const sdio_ls_clk_edge_h = Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_H_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_H_V); const sdio_ls_clk_edge_n = Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_N_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_CLK_EDGE_N_V); const sdio_ls_slf_clk_edge_sel = 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); const sdio_ls_drv_clk_edge_sel = 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); const sdio_ls_sam_clk_edge_sel = 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); const sdio_ls_slf_clk_en = Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_SLF_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_SLF_CLK_EN_V); const sdio_ls_drv_clk_en = Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_DRV_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_DRV_CLK_EN_V); const sdio_ls_sam_clk_en = Field.of(regs.HP_SYS_CLKRST_REG_SDIO_LS_SAM_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDIO_LS_SAM_CLK_EN_V); // --------------------------------------------------------------------------------- interrupts // // RINTSTS / INTMASK share one 16-bit layout plus a 2-bit per-card SDIO field at [17:16]. // `sdmmc_ll.h:35-53` names every bit; the numbers below are those, not a re-derivation. pub const Event = struct { pub const cd: u32 = 1 << 0; // card detect pub const re: u32 = 1 << 1; // response error pub const cmd_done: u32 = 1 << 2; pub const dto: u32 = 1 << 3; // data transfer over pub const txdr: u32 = 1 << 4; pub const rxdr: u32 = 1 << 5; pub const rcrc: u32 = 1 << 6; // response CRC error pub const dcrc: u32 = 1 << 7; // data CRC error pub const rto: u32 = 1 << 8; // response timeout pub const drto: u32 = 1 << 9; // data read timeout pub const hto: u32 = 1 << 10; // data starvation by host timeout pub const frun: u32 = 1 << 11; // FIFO under/overrun pub const hle: u32 = 1 << 12; // hardware locked write error pub const sbe: u32 = 1 << 13; // RX start-bit error pub const acd: u32 = 1 << 14; // auto command done pub const ebe: u32 = 1 << 15; // end-bit error pub const io_slot0: u32 = 1 << 16; pub const io_slot1: u32 = 1 << 17; /// What `sdmmc_ll.h:64-69` (SDMMC_LL_EVENT_DEFAULT) enables at init. Kept exactly as ESP-IDF /// spells it, because the oracle compares against it; what this driver actually unmasks is /// `armed`, below. pub const default: u32 = cd | re | cmd_done | dto | rcrc | dcrc | rto | drto | hto | hle | sbe | ebe; /// `default` without card detect, and the only mask `configureInterrupts` ever writes. /// /// Bit 0 has to go, and this is not a preference. There is no card-detect pin on this board: /// `configurePins` ties the signal to a matrix constant 0 ("card present"), and the /// transition it makes while doing so *latches* RINTSTS.cd. RINTSTS is a sticky /// write-1-to-clear register and nothing in the command path clears bit 0 - `sendCommand` /// deliberately writes `default & ~cd` so as not to disturb asynchronous events. So with cd /// unmasked, the controller's single output line into the CLIC is asserted from bring-up /// onwards and never deasserts, and anyone who enables that CLIC line takes an interrupt /// storm that no handler can end. Found by RxPath on CLIC line 21; the fix belongs here /// rather than in the handler, because a level output that nothing can lower is this file's /// bug. /// /// The two SDIO card-interrupt bits are absent from both masks: `setSlaveInterruptEnabled` /// turns the one for this slot on when somebody is prepared to service it. pub const armed: u32 = default & ~cd; /// Anything in here means the command failed. `sdmmc_ll.h:71-77` calls the superset /// SDMMC_LL_SD_EVENT_MASK; this is the error half of it. pub const command_errors: u32 = re | rcrc | rto | hle; pub const data_errors: u32 = dcrc | drto | hto | frun | sbe | ebe; }; /// The IDMAC's five reportable events - TI, RI, FBE, DU, CES - as one mask. `sdmmc_ll.h:83` /// SDMMC_LL_EVENT_DMA_MASK. const idsts_event_mask: u32 = 0x1f; /// The CLIC source this controller raises, for a caller that wants to be woken rather than to /// poll. Registering a handler is `hal.intr`'s job and not this file's: see the note on /// `slaveInterruptPending`. pub const interrupt_source = intr.Source.sdio_host; // -------------------------------------------------------------------------------------- cache // // The IDMAC reads its descriptors and its data buffer straight out of L2MEM. The CPU reaches the // same L2MEM through the L1 data cache (`soc_caps.h:185`, SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE), and // that cache is write-back: `esp_cache_msync(..., DIR_C2M)` exists precisely because a store the // CPU has made may still be sitting in a dirty line when the DMA engine reads memory. // // ESP-IDF offers two ways out and uses both. `sd_trans_sdmmc.c:135-139` writes descriptors through // the normal address and calls `esp_cache_msync` after every one. `gdma_link.c:100-118` does it // the other way: one write-back-and-invalidate when the region is created, and from then on every // CPU access goes through the non-cacheable alias at `addr + 0x40000000` // (`hal/cache_ll.h:27` CACHE_LL_L2MEM_NON_CACHE_ADDR, `soc/ext_mem_defs.h:68`). // // **This file takes the second route.** It is the cheaper one - no cache call in the transfer // path - and it is the only one that stays correct without a cache HAL this project does not have. // The one-time write-back-and-invalidate is still required, and skipping it is a real bug rather // than a theoretical one: `_start` clears .bss with ordinary stores (`src/main.zig:85-92`), so // every word of the DMA region below starts life as a *dirty* cache line full of zeros. Nothing // says when those lines are evicted; if one is written back after a descriptor has been prepared // through the alias, the descriptor becomes zero and the IDMAC stalls on an unowned descriptor. // `gdma_link.c:107-112` does exactly this call for exactly this reason. // // The two ROM entry points are addressed directly rather than declared `extern`, because the // generated linker script provides only `ets_printf` and `ets_delay_us`. The addresses are // ESP-IDF's, from `components/esp_rom/esp32p4/ld/esp32p4.rom.ld:186` and `:190` - the hw_ver1 // file, which is the one that matches this die. (If they move into the linker script beside the // other two, these two lines become `extern fn` and nothing else changes.) /// `soc/ext_mem_defs.h:68` SOC_NON_CACHEABLE_OFFSET. pub const non_cacheable_offset: u32 = 0x4000_0000; /// `cache_ll_l1_dcache_get_line_size` reports this on the P4, and `sdmmc_struct.h:36-38` states it /// in prose: "On P4, L1 Cache alignment is 64B". pub const cache_line: u32 = 64; /// `rom/cache.h:230` - CACHE_MAP_L1_DCACHE is BIT(4). const cache_map_l1_dcache: u32 = 1 << 4; const romCacheWriteBackAddr: *const fn (map: u32, addr: u32, size: u32) callconv(.c) c_int = @ptrFromInt(0x4fc0_03f4); const romCacheInvalidateAddr: *const fn (map: u32, addr: u32, size: u32) callconv(.c) c_int = @ptrFromInt(0x4fc0_03e4); // ------------------------------------------------------------------------------- DMA descriptor /// One IDMAC descriptor, exactly as the hardware reads it: `sdmmc_struct.h:13-41`. /// /// ESP-IDF's `sdmmc_desc_t` is 64 bytes, not 16, and its own comment says why and when not to: /// "These `reserved[12]` are for cache alignment... For those who want to access the DMA /// descriptor in a non-cacheable way, you can consider remove these `reserved[12]` bytes" /// (`sdmmc_struct.h:35-39`). That is this file, so the padding is gone and the descriptor is the /// 16 bytes the IDMAC actually fetches. pub const Descriptor = extern struct { flags: u32, /// [12:0] buffer1_size, [25:13] buffer2_size. sizes: u32, buffer1: u32, /// Also `buffer2_ptr`; which one it is depends on `second_address_chained`. next: u32, pub const disable_int_on_completion: u32 = 1 << 1; pub const last_descriptor: u32 = 1 << 2; pub const first_descriptor: u32 = 1 << 3; pub const second_address_chained: u32 = 1 << 4; pub const end_of_ring: u32 = 1 << 5; pub const card_error_summary: u32 = 1 << 30; pub const owned_by_idmac: u32 = 1 << 31; /// `sdmmc_struct.h:43` SDMMC_DMA_MAX_BUF_LEN. `buffer1_size` is 13 bits wide, so 8191 would /// fit; ESP-IDF splits at 4096 and so does the bound below. pub const max_buffer_len: u32 = 4096; }; /// Bytes of L2MEM this driver owns, and the whole of its dynamic memory: there is no allocator /// here and no allocation anywhere in the transfer path. /// /// 2 KiB of payload is chosen against what sits above: ESP-Hosted's SDIO transport moves at most /// one 1600-byte frame plus its 12-byte header per CMD53, and the largest single command this /// driver can express in block mode is 4 blocks of 512. Anything larger is split across commands /// by `transferChunked`, which is correct for both addressing modes, so the number is a /// speed/footprint trade and not a limit. pub const bounce_len: u32 = 2048; /// Descriptor and bounce buffer in one cache-line-aligned region, so the one-time maintenance call /// is one call over one range whose base and length are both multiples of 64. const DmaRegion = extern struct { desc: Descriptor, _pad: [cache_line - @sizeOf(Descriptor)]u8, buf: [bounce_len]u8, }; comptime { std.debug.assert(@sizeOf(Descriptor) == 16); std.debug.assert(@sizeOf(DmaRegion) % cache_line == 0); // One descriptor is enough only while the bounce buffer fits in one. If `bounce_len` ever // grows past 4096 this has to become a ring, and this line is what will say so. std.debug.assert(bounce_len <= Descriptor.max_buffer_len); } /// 2112 bytes: 16 of descriptor, 48 of padding to a cache line, 2048 of payload. var dma: DmaRegion align(cache_line) = std.mem.zeroes(DmaRegion); /// Addresses are `usize` rather than `u32` all the way to the register write. On this target the /// two are the same type; on the host, where the arithmetic in these helpers is unit-tested, /// `@intCast` of a real 64-bit address would panic before the test could check anything. inline fn cachedAddr(p: *const anyopaque) usize { return @intFromPtr(p); } /// The address the *CPU* must use for anything in the DMA region. The hardware gets the cached /// address - that is not an inconsistency, it is what ESP-IDF does: `gdma_link.c:268-273` hands /// `list->items` to the peripheral and `:159` writes through `list->items_nc`. The alias exists to /// change how the CPU's loads and stores are treated, and a bus master is not the CPU. inline fn uncachedAddr(p: *const anyopaque) usize { return cachedAddr(p) +% @as(usize, non_cacheable_offset); } /// An address as the 32-bit register field the hardware reads it through. inline fn busAddr(p: *const anyopaque) u32 { return @intCast(cachedAddr(p)); } inline fn descNc() *volatile Descriptor { return @ptrFromInt(uncachedAddr(&dma.desc)); } inline fn bufNc() [*]volatile u8 { return @ptrFromInt(uncachedAddr(&dma.buf)); } /// Write back and invalidate the DMA region once, so that no dirty line from `_start`'s .bss clear /// can later land on top of what the alias writes. After this, the cached alias of this region is /// never touched again by anything in this file. fn syncDmaRegionOnce() void { const base = busAddr(&dma); const len: u32 = @sizeOf(DmaRegion); _ = romCacheWriteBackAddr(cache_map_l1_dcache, base, len); _ = romCacheInvalidateAddr(cache_map_l1_dcache, base, len); } // -------------------------------------------------------------------------------------- timing // // Every wait in this file is bounded, and bounded in time rather than in loop iterations: a spin // count is a different number on every optimize level, and this board has no debugger, so a wait // that never returns is indistinguishable from a crash. // // The timebase is the RISC-V `cycle` CSR, the unprivileged shadow of `mcycle`, which is what // ESP-IDF itself reads on this part (`rv_utils.h`, because SOC_CPU_HAS_CSR_PC is not defined for // the P4) and what `src/soc.zig:116-131` already uses. It is deliberately *not* `hal.systimer`: // systimer's `init` pulses the peripheral's reset, which would make the timebase jump under any // other user, and `systimer.read` returns null when nothing has brought it up - neither is a // property a bus driver should impose on its caller. // // The CPU clock is whatever the bootloader left, measured at 90 MHz on this board and rated to // 400. Deadlines are computed at the 400 MHz *ceiling*, so on real silicon every timeout below is // between 1x and 4.4x longer than its nominal microseconds. That is the safe direction: a timeout // that fires early would turn a slow card into a spurious failure, and a timeout 4x long still // terminates. const assumed_cpu_hz_max: u32 = 400_000_000; inline fn cycleLow() u32 { return asm volatile ("csrr %[r], 0xC00" : [r] "=r" (-> u32), ); } /// A bounded wait. 32 bits of cycle counter wrap after 10.7 s at the assumed ceiling, which is an /// order of magnitude past the longest deadline here, and the wrapping subtraction is correct /// across the wrap anyway. const Deadline = struct { start: u32, budget: u32, inline fn init(us: u32) Deadline { return .{ .start = cycleLow(), .budget = us *% (assumed_cpu_hz_max / 1_000_000) }; } inline fn expired(self: Deadline) bool { return (cycleLow() -% self.start) >= self.budget; } }; /// `sd_host_private.h:62` SD_HOST_SDMMC_RESET_TIMEOUT_US. const reset_timeout_us: u32 = 5_000_000; /// `sd_host_private.h:61` SD_HOST_SDMMC_START_CMD_TIMEOUT_US - how long the CIU may take to accept /// a command word, which is a bus-side handshake and nothing to do with the card. const start_cmd_timeout_us: u32 = 1_000_000; /// How long to wait for the card's response after the command has been accepted. The controller /// has its own response timeout (TMOUT.response_timeout, 255 card clocks) and raises RTO, so this /// only has to cover the case where the controller itself never reports anything. const command_done_timeout_us: u32 = 200_000; /// Data phase. TMOUT.data_timeout is programmed to 100 ms of card clocks, matching /// `sd_host_sdmmc.c:531-533`; this outer bound is twice that. const data_done_timeout_us: u32 = 200_000; /// How long the card may hold DAT0 low before a new data command. const busy_timeout_us: u32 = 500_000; // ------------------------------------------------------------------------------------- geometry pub const Width = enum { one, four }; /// Slot 1's pads on this board, and the GPIO-matrix signal each carries. /// /// Slot 0 has a direct IO MUX function and slot 1 does not /// (`sdmmc_ll.h:88` SDMMC_LL_SLOT_SUPPORT_GPIO_MATRIX(1) is 1, and `sdmmc_periph.c:37-49` has /// -1 for every slot-1 IO MUX pin), so every slot-1 signal is routed through the matrix. The /// indices are `gpio_sig_map.h:8-18`, reached here through `regs` rather than written out: the /// same discipline `hal/gpio.zig` applies to SIG_GPIO_OUT_IDX, for the same reason. pub const Pins = struct { clk: u8, cmd: u8, d0: u8, d1: u8, d2: u8, d3: u8, }; /// The ESP32-C6 coprocessor's wiring on this board. CLK 18, CMD 19, D0-D3 = 14/15/16/17. pub const c6_pins: Pins = .{ .clk = 18, .cmd = 19, .d0 = 14, .d1 = 15, .d2 = 16, .d3 = 17 }; const sig = struct { const cclk: u32 = @intCast(regs.SD_CARD_CCLK_2_PAD_OUT_IDX); const ccmd: u32 = @intCast(regs.SD_CARD_CCMD_2_PAD_OUT_IDX); const cdata0: u32 = @intCast(regs.SD_CARD_CDATA0_2_PAD_OUT_IDX); const cdata1: u32 = @intCast(regs.SD_CARD_CDATA1_2_PAD_OUT_IDX); const cdata2: u32 = @intCast(regs.SD_CARD_CDATA2_2_PAD_OUT_IDX); const cdata3: u32 = @intCast(regs.SD_CARD_CDATA3_2_PAD_OUT_IDX); const card_detect: u32 = @intCast(regs.SD_CARD_DETECT_N_2_PAD_IN_IDX); const card_int: u32 = @intCast(regs.SD_CARD_INT_N_2_PAD_IN_IDX); comptime { // The `_2` in these names is slot 1: `sdmmc_periph.c:52-76` fills // `sdmmc_slot_gpio_sig[1]` from exactly these macros. Slot 0's set is named `_1` and would // route the wrong controller port to the C6's pads, silently. std.debug.assert(cclk == 0 and ccmd == 1 and cdata0 == 2); std.debug.assert(cdata1 == 3 and cdata2 == 4 and cdata3 == 5); // The card interrupt is sensed on D1's *input* index, and `configurePins` hands `matrixIn` // the *output* one - correct only because the P4's two signal tables agree on this signal. // `gpio_sig_map.h:13-14` gives cdata1 the number 3 in both directions, and ESP-IDF relies // on the same coincidence: `configure_pin_gpio_matrix` (`sd_host_sdmmc.c:1091-1105`) passes // one `gpio_matrix_sig` to both `esp_rom_gpio_connect_in_signal` and `..._out_signal`. // Asserted rather than assumed, because a mismatch here would route data correctly and // sense interrupts from the wrong pad - which is invisible until something waits. std.debug.assert(cdata1 == @as(u32, @intCast(regs.SD_CARD_CDATA1_2_PAD_IN_IDX))); } }; // -------------------------------------------------------------------------------------- state const State = struct { slot: u1 = 1, width: Width = .four, /// The frequency `cardInit` switches to once the card is addressed and in 4-bit mode. target_khz: u32 = 40_000, pins: Pins = c6_pins, /// Relative card address from CMD3, needed as the argument of CMD7. rca: u16 = 0, initialised: bool = false, }; var state: State = .{}; /// The card's relative address, as returned by CMD3. Zero until `cardInit` has run. pub fn rca() u16 { return state.rca; } inline fn slotBit() u32 { return @as(u32, 1) << state.slot; } // ------------------------------------------------------------------------------ command words // // One word, one function, no hardware. This is the part of the driver most worth testing on the // host, and the part the oracle can compare against ESP-IDF's own bitfield struct without going // anywhere near the card. /// Compose one field's contribution to a register word. `mmio.Reg.write` does this against a /// register; here the destination is a value, because the command word is built, checked and only /// then stored. inline fn bits(comptime f: Field, v: u32) u32 { return (v & f.unshiftedMask()) << f.shift; } const cmd_index = Field.of(regs.SDHOST_CMD_INDEX_S, regs.SDHOST_CMD_INDEX_V); const response_expect = Field.of(regs.SDHOST_RESPONSE_EXPECT_S, regs.SDHOST_RESPONSE_EXPECT_V); const response_length = Field.of(regs.SDHOST_RESPONSE_LENGTH_S, regs.SDHOST_RESPONSE_LENGTH_V); const check_response_crc = Field.of(regs.SDHOST_CHECK_RESPONSE_CRC_S, regs.SDHOST_CHECK_RESPONSE_CRC_V); const data_expected = Field.of(regs.SDHOST_DATA_EXPECTED_S, regs.SDHOST_DATA_EXPECTED_V); const read_write = Field.of(regs.SDHOST_READ_WRITE_S, regs.SDHOST_READ_WRITE_V); const transfer_mode = Field.of(regs.SDHOST_TRANSFER_MODE_S, regs.SDHOST_TRANSFER_MODE_V); const send_auto_stop = Field.of(regs.SDHOST_SEND_AUTO_STOP_S, regs.SDHOST_SEND_AUTO_STOP_V); const wait_prvdata_complete = Field.of(regs.SDHOST_WAIT_PRVDATA_COMPLETE_S, regs.SDHOST_WAIT_PRVDATA_COMPLETE_V); const stop_abort_cmd = Field.of(regs.SDHOST_STOP_ABORT_CMD_S, regs.SDHOST_STOP_ABORT_CMD_V); const send_initialization = Field.of(regs.SDHOST_SEND_INITIALIZATION_S, regs.SDHOST_SEND_INITIALIZATION_V); const card_number = Field.of(regs.SDHOST_CARD_NUMBER_S, regs.SDHOST_CARD_NUMBER_V); const update_clock_registers_only = Field.of(regs.SDHOST_UPDATE_CLOCK_REGISTERS_ONLY_S, regs.SDHOST_UPDATE_CLOCK_REGISTERS_ONLY_V); /// `sdmmc_reg.h:486-494` spells this `USE_HOLE_REG`; `sdmmc_struct.h:473` spells it /// `use_hold_reg`, which is what it is - the hold register that synchronises CMD and DATA to /// cclk_out. Same bit 29, and ESP-IDF sets it on every command (`sd_host_sdmmc.c:859-860`). const use_hold_reg = Field.of(regs.SDHOST_USE_HOLE_REG_S, regs.SDHOST_USE_HOLE_REG_V); const start_cmd = Field.of(regs.SDHOST_START_CMD_S, regs.SDHOST_START_CMD_V); pub const Response = enum { none, short, long }; pub const Direction = enum { read, write }; /// Everything that distinguishes one command from another, in the terms the register uses. pub const Command = struct { index: u6, response: Response = .none, /// Whether the controller checks the response's CRC7. Off for R3 and R4, which do not carry a /// valid one - `sd_protocol_types.h:140-141` define both without SCF_RSP_CRC, and /// `make_hw_cmd` (`sd_trans_sdmmc.c:214-216`) keys `check_response_crc` off exactly that flag. check_crc: bool = false, data: ?Direction = null, /// 80 clocks of 1 before the command. Required once after power-on, and set only for CMD0, /// which is where ESP-IDF sets it (`sd_trans_sdmmc.c:197-206`). send_init: bool = false, /// Wait for a previous data transfer to finish before sending. Set on everything except CMD0, /// CMD12 and CMD11, again following `make_hw_cmd`. wait_prvdata: bool = true, auto_stop: bool = false, stop_abort: bool = false, /// Not a command at all: push CLKDIV/CLKSRC/CLKENA into the card clock domain. update_clock: bool = false, slot: u1 = 0, }; /// The 32-bit word that, written to SDHOST_CMD_REG, issues `c`. /// /// This is `make_hw_cmd` (`sd_trans_sdmmc.c:190-229`) plus the three fields /// `sd_host_slot_start_command` adds afterwards - `use_hold_reg`, `card_num` and `start_command` /// (`sd_host_sdmmc.c:859-881`) - because those three are not optional and splitting them across /// two functions is how one of them gets forgotten. pub fn commandWord(c: Command) u32 { var w: u32 = 0; w |= bits(cmd_index, c.index); if (c.response != .none) w |= bits(response_expect, 1); if (c.response == .long) w |= bits(response_length, 1); if (c.check_crc) w |= bits(check_response_crc, 1); if (c.data) |dir| { w |= bits(data_expected, 1); if (dir == .write) w |= bits(read_write, 1); } if (c.auto_stop) w |= bits(send_auto_stop, 1); if (c.wait_prvdata) w |= bits(wait_prvdata_complete, 1); if (c.stop_abort) w |= bits(stop_abort_cmd, 1); if (c.send_init) w |= bits(send_initialization, 1); if (c.update_clock) w |= bits(update_clock_registers_only, 1); w |= bits(card_number, c.slot); // Block transfers only; `transfer_mode` selects stream mode, which no SDIO command uses. w |= bits(transfer_mode, 0); w |= bits(use_hold_reg, 1); w |= bits(start_cmd, 1); return w; } // ------------------------------------------------------------------------------ SDIO protocol // // Command indices and argument layouts, from `sd_protocol_defs.h`. Written out as constants rather // than reached through `regs` because they are the SD specification, not this chip: the register // headers know nothing about them. /// `sd_protocol_defs.h:35`, `:40`, `:61`, `:78-80`. const cmd_go_idle_state: u6 = 0; const cmd_send_relative_addr: u6 = 3; const cmd_io_send_op_cond: u6 = 5; const cmd_select_card: u6 = 7; const cmd_io_rw_direct: u6 = 52; const cmd_io_rw_extended: u6 = 53; /// CMD52's argument: `sd_protocol_defs.h:484-492`. pub fn cmd52Arg(write: bool, func: u3, addr: u17, raw_flag: bool, data: u8) u32 { var a: u32 = 0; if (write) a |= @as(u32, 1) << 31; a |= @as(u32, func) << 28; if (raw_flag) a |= @as(u32, 1) << 27; a |= @as(u32, addr) << 9; a |= data; return a; } /// CMD53's argument: `sd_protocol_defs.h:496-506`. /// /// `count` is blocks in block mode and bytes in byte mode, and it is 9 bits: 0 means 512 in byte /// mode ("See 5.3.1 SDIO simplified spec", `sdmmc_io.c:351-355`) and infinite in block mode, which /// this driver never asks for. pub fn cmd53Arg(write: bool, func: u3, addr: u17, block_mode: bool, incrementing: bool, count: u9) u32 { var a: u32 = 0; if (write) a |= @as(u32, 1) << 31; a |= @as(u32, func) << 28; if (block_mode) a |= @as(u32, 1) << 27; if (incrementing) a |= @as(u32, 1) << 26; a |= @as(u32, addr) << 9; a |= count; return a; } /// The block size this driver programmes into BLKSIZ and into the card's CCCR/FBR. /// `sdmmc_common.h:195` SDMMC_IO_BLOCK_SIZE, and ESP-Hosted writes the same 512 into FN0 and FN1 /// (`port_esp_hosted_host_sdio.c:211-217`). pub const io_block_size: u32 = 512; /// CCCR register offsets, `sd_protocol_defs.h:509-530`. pub const cccr = struct { pub const revision: u17 = 0x00; pub const fn_enable: u17 = 0x02; pub const fn_ready: u17 = 0x03; pub const int_enable: u17 = 0x04; pub const int_pending: u17 = 0x05; pub const ctl: u17 = 0x06; pub const bus_width: u17 = 0x07; pub const card_cap: u17 = 0x08; pub const cis_ptr: u17 = 0x09; pub const blksize_l: u17 = 0x10; pub const blksize_h: u17 = 0x11; pub const ctl_reset: u8 = 1 << 3; pub const bus_width_1: u8 = 0; pub const bus_width_4: u8 = 2; /// Low-speed card; and "4-bit low speed", which says a low-speed card supports 4 bits anyway. pub const card_cap_lsc: u8 = 1 << 6; pub const card_cap_4bls: u8 = 1 << 7; }; /// `sd_protocol_defs.h:533` SD_IO_FBR_START - function n's register block starts here. const fbr_start: u17 = 0x100; /// R4's fields, `sd_protocol_defs.h:478-481`. const r4_mem_ready: u32 = 1 << 31; const r4_mem_present: u32 = 1 << 27; /// The voltage window the host offers in CMD5's second pass: bits 23:15, i.e. 2.8-3.6 V. /// `sd_protocol_defs.h:109` SD_OCR_VOL_MASK, which is the whole of what `get_host_ocr` returns - /// "For now tell that the host has 2.8-3.6V voltage range" (`sdmmc_common.h:174-180`). const host_ocr: u32 = 0x00ff_8000; // ------------------------------------------------------------------------------- command issue /// Write one command word and wait for the CIU to take it. No card traffic is implied: a clock /// update command goes through here too. /// /// Both waits are the ones `sd_host_slot_start_command` performs (`sd_host_sdmmc.c:862-892`), /// bounded the same way. The first is not redundant with the second: writing any command register /// while `start_command` is still set is a hardware locked write error, and HLE is reported /// asynchronously in RINTSTS where it is easy to attribute to the wrong command. fn startCommand(word: u32, arg: u32) Error!void { var d = Deadline.init(start_cmd_timeout_us); while (cmd.get(start_cmd) != 0) { if (d.expired()) return error.Busy; } cmdarg.writeRaw(arg); cmd.writeRaw(word); d = Deadline.init(start_cmd_timeout_us); while (cmd.get(start_cmd) != 0) { if (d.expired()) return error.Timeout; } } /// Push CLKDIV, CLKSRC and CLKENA into the card clock domain. fn clockUpdate() Error!void { try startCommand(commandWord(.{ .index = 0, .update_clock = true, .wait_prvdata = true, .slot = state.slot, }), 0); } /// Turn a RINTSTS snapshot into the failure it describes. /// /// Order matters only in that the first match wins, and it is chosen so the most specific cause is /// reported: a CRC error and a timeout together is a CRC error, because the timeout is downstream /// of it. fn decodeErrors(sts: u32) Error!void { if (sts & (Event.rcrc | Event.dcrc) != 0) return error.CrcError; if (sts & (Event.rto | Event.drto | Event.hto) != 0) return error.Timeout; if (sts & (Event.re | Event.hle | Event.ebe | Event.sbe | Event.frun) != 0) return error.ResponseError; } /// Wait for one or more RINTSTS bits, failing on any error bit or on the deadline. /// /// RINTSTS is write-1-to-clear, so this reads with `raw()` and clears with `writeRaw(mask)` - /// never `modify`, which would clear every bit it read back and lose the events this function is /// not waiting for. fn waitEvents(want: u32, errors: u32, us: u32) Error!u32 { const d = Deadline.init(us); while (true) { const sts = rintsts.raw(); if (sts & errors != 0) { rintsts.writeRaw(sts & (want | errors)); try decodeErrors(sts & errors); // Every bit any caller passes in `errors` is covered above; a new one arriving here // is a bug in this file, and reporting it beats an `unreachable` on a board with no // debugger. return error.ResponseError; } if (sts & want == want) { rintsts.writeRaw(want); return sts; } if (d.expired()) return error.Timeout; } } /// A command with no data phase: issue it, wait for command-done, return R1/R5's first word. fn sendCommand(c: Command, arg: u32) Error!u32 { // Everything this command is about to overwrite. This slot's SDIO card interrupt is // deliberately left alone - the C6 raises it asynchronously and clearing it here would drop a // wakeup the layer above is waiting for - and `clearNonSlaveInterrupts` is exactly that set. // // It used to be `Event.default & ~Event.cd`, which is a *subset* of the event bits and left // four of them latched for ever: txdr(4), rxdr(5), frun(11) and acd(14). Two consequences, one // cosmetic and one not. Cosmetic: every RINTSTS a diagnostic prints carries a stale 0x10 from // the first transfer onwards, which is noise in exactly the register that has to be read // carefully. Not cosmetic: **frun is a member of `Event.data_errors`**, so one FIFO // under/overrun - ever - would latch a bit that nothing clears and fail every subsequent // `waitEvents(Event.dto, Event.data_errors, ...)` for the rest of the run. The data path works // today only because frun has never fired. clearNonSlaveInterrupts(); var cc = c; cc.slot = state.slot; try startCommand(commandWord(cc), arg); _ = try waitEvents(Event.cmd_done, Event.command_errors, command_done_timeout_us); return resp0.raw(); } /// R5's status byte, the one CMD52 and CMD53 return. `sd_protocol_defs.h:493` takes the data byte; /// the flags above it say whether the card accepted the command at all. const r5_com_crc_error: u32 = 1 << 15; const r5_illegal_command: u32 = 1 << 14; const r5_error: u32 = 1 << 11; const r5_function_number: u32 = 1 << 9; const r5_out_of_range: u32 = 1 << 8; const r5_bad: u32 = r5_com_crc_error | r5_illegal_command | r5_error | r5_function_number | r5_out_of_range; fn checkR5(r: u32) Error!u8 { if (r & r5_com_crc_error != 0) return error.CrcError; if (r & r5_bad != 0) return error.ResponseError; return @truncate(r); } // ------------------------------------------------------------------------------- bring-up /// Controller, FIFO and DMA reset, then wait for all three to self-clear. /// /// All three bits are self-clearing, and `sdmmc_ll.h:486`, `:510` and `:534` each say so with a /// different delay ("two AHB clock cycles", "after reset done"). ESP-IDF sets all three and polls /// all three together (`sd_host_sdmmc.c:917-950`), which is what makes one bounded wait correct /// for the set. pub fn resetController() Error!void { ctrl.modify(.{ controller_reset.is(1), fifo_reset.is(1), dma_reset.is(1) }); const d = Deadline.init(reset_timeout_us); while (true) { const v = ctrl.raw(); if (v & (controller_reset.mask() | fifo_reset.mask() | dma_reset.mask()) == 0) return; if (d.expired()) return error.Timeout; } } /// The interrupt configuration `sd_host_sdmmc.c:120-124` establishes - clear everything, mask /// everything, then unmask the completion and error events and turn the global enable on - with /// one deliberate deviation: card detect stays masked *and* gets cleared. See `Event.armed` for /// why that bit is load-bearing on a board with no card-detect pin. /// /// `int_enable` gates the controller's single line into the CLIC. It is on even though this driver /// polls, because RINTSTS is set regardless and the layer above may register a handler for the /// SDIO card interrupt; leaving it off would mean `setSlaveInterruptEnabled(true)` silently did /// nothing. pub fn configureInterrupts() void { rintsts.writeRaw(0xffff_ffff); intmask.writeRaw(0); ctrl.modify(.{int_enable.is(0)}); intmask.writeRaw(Event.armed); // Belt and braces: `armed` keeps the controller from reporting a latched cd, and this makes // sure there is no latched cd to report if anything ever unmasks it again. rintsts.writeRaw(Event.cd); ctrl.modify(.{int_enable.is(1)}); } /// `sdmmc_ll_init_dma`, `sdmmc_ll.h:796-804`: enable the DMA path, clear the bus-mode register, /// pulse the IDMAC's own software reset, and unmask its three completion interrupts. pub fn initDma() void { ctrl.modify(.{dma_enable.is(1)}); bmod.writeRaw(0); bmod.modify(.{bmod_swr.is(1)}); idinten.modify(.{ idinten_ni.is(1), idinten_ri.is(1), idinten_ti.is(1) }); } /// Leave the controller's interrupt output silent, and both status registers clean. /// /// `configureInterrupts` and `initDma` above are ESP-IDF's sequences, and ESP-IDF is /// interrupt-driven: its transfers wait on a queue its ISR fills, so it needs command-done, the /// error bits and the IDMAC's completions in the masks. **This driver polls**, so every one of /// those is noise on a line whose only handler understands one cause. Worse than noise: two of /// them hold the line asserted forever. /// /// * **INTMASK** gates RINTSTS into MINTSTS. Zero here costs nothing - `waitEvents` reads /// RINTSTS, and "Bits are logged regardless of interrupt mask status" /// (`sdmmc_struct.h:589-591`). /// * **IDINTEN** gates the IDMAC's own events, and it does *not* go through INTMASK. `initDma` /// enables NI/RI/TI because `sdmmc_ll_init_dma` does, and IDF can afford that because its ISR /// clears IDSTS on every interrupt (`sd_host_sdmmc.c:801-802`). `dataTransfer` clears IDSTS /// *before* a transfer and nothing clears it after, so RI and its sticky summary NIS stay set /// from the first CMD53 onwards - a permanently asserted interrupt line that no INTMASK write /// can lower. /// /// `CTRL.int_enable` stays on: with both masks at zero the line cannot assert anyway, and leaving /// the global enable alone keeps `armSlaveInterrupt` down to the stores that matter. pub fn muteInterrupts() void { intmask.writeRaw(0); idinten.writeRaw(0); rintsts.writeRaw(0xffff_ffff); idsts.writeRaw(idsts_event_mask); } /// FIFO watermarks and DMA burst size. /// /// ESP-IDF never writes this register on any target - there is no `sdmmc_ll` function for it and /// no assignment anywhere in `components/` - so the value in the measured working dump, /// `fifoth=0x01FF0000`, is the hardware's reset state: rx watermark 511, tx watermark 0, burst /// size code 0 (one transfer). This function writes that value explicitly rather than inheriting /// it, because a controller reset is not the only thing that can have touched the register and /// "the same as reset" is a claim worth making in code. /// /// It is also a performance knob left deliberately untouched: DesignWare recommends half the FIFO /// depth for both watermarks and a burst size matching the AXI port, and tx watermark 0 means a /// DMA request only when the FIFO is completely empty. Turning that knob without a board to /// measure on would be guessing, and the guess would be against a configuration known to work at /// 40 MHz. pub fn setFifoThreshold(rx: u32, tx: u32, msize: u32) void { fifoth.write(.{ rx_wmark.is(rx), tx_wmark.is(tx), dma_msize.is(msize) }); } /// The reset-value watermarks, which are the ones the working dump shows. pub const default_rx_watermark: u32 = 511; pub const default_tx_watermark: u32 = 0; pub const default_dma_msize: u32 = 0; /// Bus width, host side. The card side is a CCCR write and is done in `cardInit`; the two must /// change in that order, or the next command goes out on a bus the card is not listening to. pub fn setBusWidth(w: Width) void { const m = slotBit(); const c8 = ctype.get(card_width8) & ~m; const c4 = switch (w) { .one => ctype.get(card_width4) & ~m, .four => ctype.get(card_width4) | m, }; ctype.modify(.{ card_width4.is(c4), card_width8.is(c8) }); } pub fn setBlockSize(bytes: u32) void { blksiz.modify(.{block_size.is(bytes)}); } /// The two-stage divider, resolved. Stage one is `host_div` in HP_SYS_CLKRST, stage two is the /// controller's own CLKDIV, and the card clock is `160 MHz / host_div / (2 * card_div)` with /// `card_div == 0` meaning bypass. /// /// The table is `sd_host_slot_get_clk_dividers` (`sd_host_sdmmc.c:998-1062`), restricted to the /// PLL160M source: this board's C6 is a 3.3 V SDIO device, so the 200 MHz SDIO PLL and the UHS-I /// speeds it exists for are out of reach and out of scope. pub const Dividers = struct { host: u32, card: u32 }; pub fn dividersFor(khz: u32) Dividers { const src_hz: u32 = 160_000_000; if (khz >= 40_000) return .{ .host = 4, .card = 0 }; // 160/4 = 40 MHz if (khz == 20_000) return .{ .host = 8, .card = 0 }; // 160/8 = 20 MHz if (khz == 400) return .{ .host = 10, .card = 20 }; // 160/10/(20*2) = 400 kHz var host = src_hz / (khz * 1000); var card: u32 = 0; if (host > 15) { host = 2; card = (src_hz / 2) / (2 * khz * 1000); if (((src_hz / 2) % (2 * khz * 1000)) > 0) card += 1; } else if (src_hz % (khz * 1000) > 0) { host += 1; } return .{ .host = host, .card = card }; } /// Stage one: the clock generator in HP_SYS_CLKRST. `sdmmc_ll_set_clock_div`, /// `sdmmc_ll.h:244-258`. /// /// The `edge_cfg_update` bit is write-to-trigger and must be pulsed - set then cleared - after the /// three edge fields, or the new division is programmed and never latched. pub fn setHostClockDiv(div: u32) void { if (div > 1) { peri_clk_ctrl02.modify(.{ sdio_ls_clk_edge_h.is(div / 2 - 1), sdio_ls_clk_edge_n.is(div - 1), sdio_ls_clk_edge_l.is(div - 1), }); peri_clk_ctrl02.modify(.{sdio_ls_clk_edge_cfg_update.is(1)}); peri_clk_ctrl02.modify(.{sdio_ls_clk_edge_cfg_update.is(0)}); } else { peri_clk_ctrl01.modify(.{sdio_hs_mode.is(1)}); peri_clk_ctrl02.modify(.{ sdio_ls_clk_edge_h.is(0), sdio_ls_clk_edge_n.is(0), sdio_ls_clk_edge_l.is(0), }); } } /// PLL160M, the only source this driver uses. `sdmmc_ll_select_clk_source`, `sdmmc_ll.h:212-229`: /// source value 0 is PLL160M and 1 is the 200 MHz SDIO PLL. pub fn selectPll160m() void { peri_clk_ctrl01.modify(.{ sdio_ls_clk_src_sel.is(0), sdio_ls_clk_en.is(1) }); } /// The driving, sampling and self clocks the pad logic runs on. `sdmmc_ll_init_phase_delay`, /// `sdmmc_ll.h:303-315`. Without this the three gates stay off and the bus does not move, which is /// the kind of failure that looks like a wiring fault. pub fn initPhaseDelay() void { peri_clk_ctrl02.modify(.{ sdio_ls_drv_clk_en.is(1), sdio_ls_sam_clk_en.is(1), sdio_ls_slf_clk_en.is(1), sdio_ls_drv_clk_edge_sel.is(1), sdio_ls_sam_clk_edge_sel.is(0), sdio_ls_slf_clk_edge_sel.is(0), }); peri_clk_ctrl02.modify(.{sdio_ls_clk_edge_cfg_update.is(1)}); peri_clk_ctrl02.modify(.{sdio_ls_clk_edge_cfg_update.is(0)}); } /// Stage one, whole: divider, source, phase clocks, and the settle the hardware needs afterwards. /// `sd_host_set_clk_div`, `sd_host_sdmmc.c:974-990`, including its closing /// `esp_rom_delay_us(10)` - "Wait for the clock to propagate". /// /// This has to happen before the controller reset, not after. `controller_reset` is documented to /// self-clear "after two AHB and two sdhost_cclk_in clock cycles" (`sdmmc_reg.h:18-20`), so with /// no card clock reaching the block the bit never clears and the reset wait runs to its full /// timeout. ESP-IDF's order says the same thing without saying it: `sd_host_set_clk_div` at /// `sd_host_sdmmc.c:109`, `sd_host_reset` at `:112`. pub fn setHostClock(div: u32) void { setHostClockDiv(div); selectPll160m(); initPhaseDelay(); spinMicros(10); } /// Stage two: the controller's per-slot divider and the divider-to-slot mux. /// `sdmmc_ll_set_card_clock_div`, `sdmmc_ll.h:431-442`. Slot 1 uses divider 1, slot 0 uses divider /// 0 - so the mux value equals the slot number, which is why one line covers both. pub fn setCardClockDiv(div: u32) void { if (state.slot == 0) { clksrc.modify(.{clksrc_card0.is(0)}); clkdiv.modify(.{clk_divider0.is(div)}); } else { clksrc.modify(.{clksrc_card1.is(1)}); clkdiv.modify(.{clk_divider1.is(div)}); } } /// The card clock's on/off switch, one bit per slot. Takes effect only after a clock update /// command. `sdmmc_ll_enable_card_clock`, `sdmmc_ll.h:415-422`. pub fn setCardClockEnabled(on: bool) void { const cur = clkena.get(cclk_enable); clkena.modify(.{cclk_enable.is(if (on) cur | slotBit() else cur & ~slotBit())}); } /// Stop the card clock while the card is idle. `sdmmc_ll_enable_card_clock_low_power`, /// `sdmmc_ll.h:474-481`. **Off** for SDIO: the card raises its interrupt on D1 and cannot do so /// with the clock stopped, which is why ESP-IDF clears the same bit for any slot with /// `cclk_always_on` (`sd_host_sdmmc.c:272-285`) and why the measured working dump reads /// `clkena=0x00000002` rather than `0x00020002`. pub fn setCardClockLowPower(on: bool) void { const cur = clkena.get(lp_enable); clkena.modify(.{lp_enable.is(if (on) cur | slotBit() else cur & ~slotBit())}); } /// Bytes in the next data transfer. `sdmmc_ll_set_data_transfer_len`, `sdmmc_ll.h:651-654`. pub fn setDataTransferLen(len: u32) void { bytcnt.modify(.{byte_count.is(len)}); } /// Data-read and response timeouts, both in card output clocks. /// `sdmmc_ll_set_data_timeout` / `sdmmc_ll_set_response_timeout`, `sdmmc_ll.h:564-582`. pub fn setTimeouts(data_cycles: u32, response_cycles: u32) void { tmout.write(.{ data_timeout.is(if (data_cycles > 0xff_ffff) 0xff_ffff else data_cycles), response_timeout.is(response_cycles), }); } /// Turn the internal DMA path on or off: both CTRL bits and both BMOD bits, together. /// `sdmmc_ll_enable_dma`, `sdmmc_ll.h:812-818`. pub fn setDmaEnabled(on: bool) void { const v: u32 = @intFromBool(on); ctrl.modify(.{ dma_enable.is(v), use_internal_dma.is(v) }); bmod.modify(.{ bmod_de.is(v), bmod_fb.is(v) }); } /// Where the IDMAC fetches its first descriptor. `sdmmc_ll_set_desc_addr`, `sdmmc_ll.h:673-676`. /// The address is the *cached* one; see the "Cache" section above for why that is right. pub fn setDescriptorAddr(a: u32) void { dbaddr.writeRaw(a); } /// Change the card clock, safely: stop it, reprogramme both stages, start it again, with a clock /// update command after each step. `sd_host_slot_set_card_clk`, `sd_host_sdmmc.c:487-537`. /// /// Low-power mode is left **off**, which is the one place this deviates from a plain SD host and /// matches the measured dump (`clkena=0x00000002`: clock enabled for slot 1, `lp_enable` clear). /// `clkena.lp_enable` stops cclk while the card is idle; an SDIO card signals its interrupt on D1 /// and needs the clock running to do it, which is why ESP-IDF turns the same bit off for any slot /// with `cclk_always_on` (`sd_host_sdmmc.c:272-285`). pub fn setBusClock(khz: u32) Error!void { const d = dividersFor(khz); setCardClockEnabled(false); try clockUpdate(); setCardClockDiv(d.card); setHostClock(d.host); try clockUpdate(); setCardClockEnabled(true); setCardClockLowPower(false); try clockUpdate(); // 100 ms of card clocks for data, and the maximum 255 card clocks for a response - "always set // response timeout to highest value, it's small enough anyway" (`sd_host_sdmmc.c:534-535`). setTimeouts(100 * khz, 255); } /// Route slot 1's six signals to the C6's pads. /// /// Pull-ups: **the board provides them externally and this enables the internal ones anyway**, on /// all six pads, because that is what the working configuration does. It is not obvious from /// ESP-Hosted's side - it leaves `SDMMC_SLOT_FLAG_INTERNAL_PULLUP` clear /// (`SDMMC_SLOT_CONFIG_DEFAULT`, `sdmmc_default_configs.h:98`: `.flags = 0`) - but every pad still /// gets one, because `configure_pin_gpio_matrix` opens with `gpio_reset_pin` /// (`sd_host_sdmmc.c:1096`) and that function enables the pull-up unconditionally: "for powersave /// reasons, the GPIO should not be floating, select pullup" (`gpio.c:469-472`). The 40 MHz link /// that produced the register dump therefore had both the module's external pull-ups and these. /// Matching a measured configuration beats reasoning about which resistor is redundant. /// /// D1 has a second job: it is the SDIO interrupt line, and the controller derives that interrupt /// from the same routed data signal - `sd_host_slot_sdmmc_io_int_enable` (`sd_host_sdmmc.c:381-388`) /// is *only* `configure_pin(d1, sdmmc_slot_gpio_sig[slot].d1, GPIO_MODE_INPUT_OUTPUT)`, the same /// two matrix writes and the same `fun_ie` the loop below already does, and it touches no /// controller register at all. Both halves are load-bearing and neither is visible in a working /// data path: with `fun_ie` clear, or with the *input* side of the matrix left pointing elsewhere, /// D1 still drives and every transfer still completes while the controller samples a constant and /// never latches a card interrupt. A link that carries traffic and never reports an event is /// exactly what that failure looks like, which is why D1 is routed both ways even in 1-bit mode /// and why `interruptDiagnostics` prints both bits. /// /// D3 is *not* routed to the controller yet. It is driven high as a plain GPIO output until the /// bus is switched to 4 bits, which is how a host tells an SDIO card to use SD mode rather than /// SPI mode; `sd_host_sdmmc.c:1282-1294` does the same and `cardInit` reconnects it at /// `sd_host_sdmmc.c:575-583`'s point in the sequence. pub fn configurePins(pins: Pins) void { // CLK is output-only. gpio.matrixOut(pins.clk, sig.cclk); gpio.setInputEnable(pins.clk, false); gpio.setPull(pins.clk, .up); const bidir = [_]struct { pin: u8, signal: u32 }{ .{ .pin = pins.cmd, .signal = sig.ccmd }, .{ .pin = pins.d0, .signal = sig.cdata0 }, .{ .pin = pins.d1, .signal = sig.cdata1 }, .{ .pin = pins.d2, .signal = sig.cdata2 }, }; for (bidir) |b| { gpio.matrixOut(b.pin, b.signal); gpio.matrixIn(b.pin, b.signal); gpio.setInputEnable(b.pin, true); gpio.setPull(b.pin, .up); } // D3 high, as a GPIO, until the bus width changes. gpio.configureOutput(pins.d3, .{ .readback = true }); gpio.setPull(pins.d3, .up); gpio.setHigh(pins.d3); // Card detect and the card's own interrupt-request pin are not wired to anything on this // board, so both are tied off in the matrix exactly as ESP-IDF ties them when no pin is // configured: card-detect to a constant 0 ("card present", `sd_host_sdmmc.c:1315-1319`) and // card-int-n to a constant 1, i.e. inactive (`:1304-1306`). Leaving them unrouted is not the // same thing: GPIO_FUNCn_IN_SEL_CFG resets with `sig_in_sel` clear, which bypasses the matrix // and takes the signal from whatever direct pad function exists - and slot 1 has none. // Write protect is left alone; nothing in this driver reads WRTPRT. gpio.matrixIn(gpio.matrix_const_zero, sig.card_detect); gpio.matrixIn(gpio.matrix_const_one, sig.card_int); } /// Reconnect D3 to the controller, once the card is in 4-bit mode. fn attachD3() void { gpio.matrixOut(state.pins.d3, sig.cdata3); gpio.matrixIn(state.pins.d3, sig.cdata3); gpio.setInputEnable(state.pins.d3, true); gpio.setPull(state.pins.d3, .up); } /// Everything from the clock gate to a controller that will accept a command, with the bus at the /// 400 kHz probing frequency and 1 bit wide - which is where an SDIO card has to be met. /// /// `cardInit` is what raises it to `khz` and to `width`, after the card has been addressed. pub fn init(opts: struct { slot: u1 = 1, width: Width = .four, khz: u32 = 40_000, pins: Pins = c6_pins, }) Error!void { state = .{ .slot = opts.slot, .width = opts.width, .target_khz = opts.khz, .pins = opts.pins, }; // The C6 hangs off slot 1 and slot 0's pads are the P4's own flash on most boards; refusing // here is cheaper than debugging a bricked boot. if (opts.slot != 1) return error.NotSupported; // 1. Bus clock and reset. Unlike most of this chip, SDMMC's bus clock is gated *off* at // power-on (HP_SYS_CLKRST SOC_CLK_CTRL1 REG_SDMMC_SYS_CLK_EN, default 0), so this is a // prerequisite and not a formality - without it the register block reads stale nonsense. // Its reset bit is not in HP_SYS_CLKRST at all but in LP_AON_CLKRST; see hal/clkrst.zig. clkrst.init(.sdmmc); // 2. The host clock generator, *before* the controller reset and not after. `sd_host_reset` // polls three self-clearing bits, and `controller_reset` clears only "after two AHB and // two sdhost_cclk_in clock cycles" (`sdmmc_reg.h:18-20`) - with no card clock reaching the // block that poll runs to its full timeout. ESP-IDF's controller init has the same order: // `sd_host_set_clk_div(ctlr, SDMMC_CLK_SRC_DEFAULT, 2)` at `sd_host_sdmmc.c:109`, then // `sd_host_reset` at `:112`. Divider 2 is IDF's provisional value, replaced at step 6. setHostClock(2); // 3. Controller, FIFO and DMA out of reset. try resetController(); // 4. Interrupts and DMA, before any command can produce one. The first two reproduce ESP-IDF // for the differential; `muteInterrupts` then takes back everything this driver polls for // instead of being interrupted by, leaving the line into the CLIC silent until a waiter // arms it. configureInterrupts(); initDma(); muteInterrupts(); // 5. Pads. After the clock gate so the controller's outputs are real, before the card clock so // the first cycle the C6 sees is a clean one. configurePins(state.pins); // 6. Bus clock at probing speed, 1 bit wide. An SDIO card has to be met at 400 kHz in 1-bit // mode; `cardInit` raises both once the card has been addressed. try setBusClock(400); setBusWidth(.one); // 7. Transfer geometry. setBlockSize(io_block_size); setFifoThreshold(default_rx_watermark, default_tx_watermark, default_dma_msize); setDescriptorAddr(busAddr(&dma.desc)); // 8. The one cache operation in this driver's life. See the "Cache" section above. syncDmaRegionOnce(); state.initialised = true; // The one claim in this sequence with no differential case behind it, said out loud once, at // the moment it is true. `configureInterrupts` and `initDma` are compared against ESP-IDF on // the die; `muteInterrupts` cannot be - it is a deliberate deviation from IDF's ISR-driven // design, and a reference implementation of our own decision would prove nothing. This line is // the substitute, and it is worth a print because both zeros are load-bearing: a non-zero // idinten here is an interrupt line that no INTMASK write can ever lower. note("MARK SDMMC_INIT intmask=0x%08x idinten=0x%08x expect 0x00000000 and 0x00000000\r\n", .{ intmask.raw(), idinten.raw(), }); } // ------------------------------------------------------------------------------ card bring-up /// CMD0, CMD5, CMD3, CMD7, then the CCCR writes that make function 1 usable: the sequence that /// takes the C6 from "powered" to "answers CMD52". /// /// The command half follows `sdmmc_card_init` (`sdmmc_init.c:78-133`) restricted to the SDIO path: /// `sdmmc_io_reset`, CMD0, `sdmmc_init_io` (CMD5 twice), `sdmmc_init_rca` (CMD3), /// `sdmmc_init_select_card` (CMD7). The CCCR half is ESP-Hosted's `hosted_sdio_card_fn_init` /// (`port_esp_hosted_host_sdio.c:143-220`) - enable function 1, wait for it to report ready, /// unmask its interrupt, switch to 4 bits, set both block sizes to 512 - because that is what this /// particular device needs and IDF's generic SDIO init does not do. /// /// The CMD52 that resets the card is allowed to fail. A device that is already out of reset /// answers it; one that is not may time out, and `sdmmc_io_reset` (`sdmmc_io.c:66-83`) accepts /// exactly that. pub fn cardInit() Error!void { if (!state.initialised) return error.NotSupported; // CCCR CTL bit 3: I/O reset. Best-effort, as above. cmd52Write(0, cccr.ctl, cccr.ctl_reset) catch {}; // CMD0 with the 80-clock init sequence and no response. _ = try sendCommand(.{ .index = cmd_go_idle_state, .response = .none, .send_init = true, .wait_prvdata = false, }, 0); // SDMMC_GO_IDLE_DELAY_MS (`sdmmc_common.h:34`), which `sdmmc_send_cmd_go_idle_state` waits // out before returning (`sdmmc_cmd.c:114-116`). CMD0 has no response, so there is nothing to // wait *for*: this is the card's own settling time and skipping it makes the next command a // coin toss. spinMicros(20_000); // CMD5 with a zero argument asks "are you an IO card, and what voltages do you take"; R4 has // no CRC, hence `check_crc = false` (`sd_protocol_types.h:141`). const probe = try sendCommand(.{ .index = cmd_io_send_op_cond, .response = .short, .check_crc = false, }, 0); const functions = (probe >> 28) & 0x7; if (functions == 0) return error.NotSupported; // answered CMD5, but has no IO function // CMD5 again with the voltage window, until the card reports ready. 100 attempts is // `sdmmc_io.c:240`; the 10 ms between them is SDMMC_IO_SEND_OP_COND_DELAY_MS // (`sdmmc_common.h:35`), spent here as a bounded spin rather than a scheduler delay. const ocr = host_ocr & probe; var ready = false; var tries: u32 = 0; while (tries < 100) : (tries += 1) { const r = try sendCommand(.{ .index = cmd_io_send_op_cond, .response = .short, .check_crc = false, }, ocr); if (r & r4_mem_ready != 0) { ready = true; break; } spinMicros(10_000); } if (!ready) return error.Timeout; // CMD3: the card picks its own relative address and returns it in R6[31:16]. const r6 = try sendCommand(.{ .index = cmd_send_relative_addr, .response = .short, .check_crc = true, }, 0); state.rca = @truncate(r6 >> 16); // CMD7 with that address moves the card from stand-by to transfer state. Every CMD52 and // CMD53 after this is addressed to it implicitly. _ = try sendCommand(.{ .index = cmd_select_card, .response = .short, .check_crc = true, }, @as(u32, state.rca) << 16); // ---- CCCR: function 1 on. const ioe = try cmd52Read(0, cccr.fn_enable); try cmd52Write(0, cccr.fn_enable, ioe | 0x02); // Wait for IOR bit 1. ESP-Hosted polls with a 10 ms gap and gives up after SDIO_INIT_MAX_RETRY // (`port_esp_hosted_host_sdio.c:177-192`). var fn_ready = false; tries = 0; while (tries < 100) : (tries += 1) { if ((try cmd52Read(0, cccr.fn_ready)) & 0x02 != 0) { fn_ready = true; break; } spinMicros(10_000); } if (!fn_ready) return error.Timeout; // Master interrupt enable plus function 1's, so the C6 can raise D1. const ie = try cmd52Read(0, cccr.int_enable); try cmd52Write(0, cccr.int_enable, ie | 0x01 | 0x02); // ---- Bus width: card first, then host, then D3 joins the bus. if (state.width == .four) { const cap = try cmd52Read(0, cccr.card_cap); // "Not a low-speed card" or "a low-speed card that supports 4 bits" - `sdmmc_io.c:182-183`. if ((cap & cccr.card_cap_lsc) == 0 or (cap & cccr.card_cap_4bls) != 0) { try cmd52Write(0, cccr.bus_width, cccr.bus_width_4); setBusWidth(.four); attachD3(); } else { state.width = .one; } } // ---- Block size 512 for function 0 and function 1, host side and card side. try setCardBlockSize(0, io_block_size); try setCardBlockSize(1, io_block_size); setBlockSize(io_block_size); // ---- Finally the target frequency, now that the card is addressed and the bus is wide. try setBusClock(state.target_khz); } /// The 16-bit block size lives in two consecutive byte registers, low half first /// (`port_esp_hosted_host_sdio.c:123-141`). Function n's copy is at `0x100 * n + 0x10`. fn setCardBlockSize(func: u3, bytes: u16) Error!void { const base: u17 = fbr_start * @as(u17, func); try cmd52Write(0, base + cccr.blksize_l, @truncate(bytes)); try cmd52Write(0, base + cccr.blksize_h, @truncate(bytes >> 8)); } /// A bounded busy-wait, for the two places the SDIO specification asks for a delay between /// retries. Same conservative frequency assumption as `Deadline`, in the same safe direction: on /// this 90 MHz die a 10 ms request takes about 44 ms. fn spinMicros(us: u32) void { const d = Deadline.init(us); while (!d.expired()) {} } // ------------------------------------------------------------------------------------- CMD52 /// Read one byte from the card's register space. /// /// This is the whole minimal milestone: after `init` and `cardInit`, `cmd52Read(0, 0x00)` reads /// CCCR offset 0 and the byte that comes back is the C6 answering. pub fn cmd52Read(func: u3, addr: u17) Error!u8 { const r = try sendCommand(.{ .index = cmd_io_rw_direct, .response = .short, .check_crc = true, }, cmd52Arg(false, func, addr, false, 0)); return checkR5(r); } /// Write one byte. The RAW flag is not set, matching `sdmmc_io_rw_direct` with SD_ARG_CMD52_WRITE /// alone (`sdmmc_io.c:187`); `sdmmc_io_write_byte` adds SD_ARG_CMD52_EXCHANGE when it wants the /// previous value back, which no caller here does. pub fn cmd52Write(func: u3, addr: u17, value: u8) Error!void { const r = try sendCommand(.{ .index = cmd_io_rw_direct, .response = .short, .check_crc = true, }, cmd52Arg(true, func, addr, false, value)); _ = try checkR5(r); } // ------------------------------------------------------------------------------------- CMD53 /// How one CMD53 is split. Two rules decide it, and both come from ESP-IDF rather than from the /// SDIO specification, because both are properties of this controller: /// /// * **Block mode when the length is a whole number of 512-byte blocks**, byte mode otherwise. /// In byte mode the count field is bytes and 0 encodes 512 ("See 5.3.1 SDIO simplified spec", /// `sdmmc_io.c:351-355`), so one byte-mode command reaches 512 bytes and no further. /// * **A byte-mode length of 4 or more must be a multiple of 4.** `sd_trans_sdmmc.c:526-532` /// rejects anything else outright, and `sdmmc_io_read_bytes` works around it by splitting: /// "host quirk: SDIO transfer with length not divisible by 4 bytes has to be split into two /// transfers: one with aligned length, the other one for the remaining 1-3 bytes" /// (`sdmmc_io.c:400-419`). So 6 bytes is two commands, 4 then 2, and 3 bytes is one. /// /// A caller that wants the split to be explicit - ESP-Hosted's block path does, because its /// addresses increment across the split - can hand over one whole-block chunk at a time and get /// exactly one block-mode command per call. A caller that does not can hand over any length. const Chunk = struct { block_mode: bool, /// Bytes in this command. len: u32, /// The CMD53 count field: blocks in block mode, bytes in byte mode with 0 meaning 512. count: u9, }; fn nextChunk(remaining: u32) Chunk { if (remaining >= io_block_size and remaining % io_block_size == 0) { const max_blocks = bounce_len / io_block_size; var blocks = remaining / io_block_size; if (blocks > max_blocks) blocks = max_blocks; return .{ .block_mode = true, .len = blocks * io_block_size, .count = @intCast(blocks), }; } var len = remaining; if (len > io_block_size) len = io_block_size; // The 4-byte rule. Below 4 bytes the whole request goes in one command; at or above it, the // aligned part goes first and the 1-3 byte tail becomes the next chunk. if (len >= 4 and len % 4 != 0) len &= ~@as(u32, 3); return .{ .block_mode = false, .len = len, .count = if (len == io_block_size) 0 else @intCast(len), }; } pub fn cmd53Read(func: u3, addr: u17, buf: []u8, incrementing: bool) Error!void { var offset: u32 = 0; var a: u32 = addr; while (offset < buf.len) { const c = nextChunk(@intCast(buf.len - offset)); const arg = cmd53Arg(false, func, @truncate(a), c.block_mode, incrementing, c.count); try dataTransfer(.read, arg, c.len, if (c.block_mode) io_block_size else c.len); const dst = buf[offset..][0..c.len]; const src = bufNc(); for (dst, 0..) |*b, i| b.* = src[i]; offset += c.len; if (incrementing) a += c.len; } } pub fn cmd53Write(func: u3, addr: u17, data: []const u8, incrementing: bool) Error!void { var offset: u32 = 0; var a: u32 = addr; while (offset < data.len) { const c = nextChunk(@intCast(data.len - offset)); const src = data[offset..][0..c.len]; const dst = bufNc(); for (src, 0..) |b, i| dst[i] = b; // The IDMAC moves whole words, so a length that is not a multiple of 4 is rounded up // (`sd_trans_sdmmc.c:127`). Zero the pad rather than send whatever the last transfer left. var pad = c.len; while (pad % 4 != 0) : (pad += 1) dst[pad] = 0; const arg = cmd53Arg(true, func, @truncate(a), c.block_mode, incrementing, c.count); try dataTransfer(.write, arg, c.len, if (c.block_mode) io_block_size else c.len); offset += c.len; if (incrementing) a += c.len; } } /// One CMD53 with its data phase, through the IDMAC and the bounce buffer. /// /// Order is ESP-IDF's (`sd_trans_sdmmc.c:524-568`): descriptor and transfer registers first, then /// the command word, then wait for command-done and data-transfer-over in that order. Preparing /// the DMA after starting the command would be a race against a card that answers immediately. fn dataTransfer(dir: Direction, arg: u32, len: u32, blk: u32) Error!void { std.debug.assert(len <= bounce_len); // The card must not still be holding DAT0 low from a previous write. const busy = Deadline.init(busy_timeout_us); while (status.get(data_busy) != 0) { if (busy.expired()) return error.Busy; } // As in `sendCommand`: the whole event set except this slot's card interrupt. `frun` is in // `Event.data_errors` and nothing else ever clears it. clearNonSlaveInterrupts(); idsts.writeRaw(idsts_event_mask); const padded = (len + 3) & ~@as(u32, 3); const d = descNc(); d.buffer1 = busAddr(&dma.buf); d.next = 0; d.sizes = padded; // buffer1_size is [12:0]; buffer2 is unused d.flags = Descriptor.owned_by_idmac | Descriptor.first_descriptor | Descriptor.last_descriptor | Descriptor.second_address_chained; setDataTransferLen(len); setBlockSize(blk); setDescriptorAddr(busAddr(&dma.desc)); // `sdmmc_ll_enable_dma`, `sdmmc_ll.h:812-818`, then the poll demand that tells the IDMAC to // re-read a descriptor it may have parked on. setDmaEnabled(true); pldmnd.writeRaw(1); try startCommand(commandWord(.{ .index = cmd_io_rw_extended, .response = .short, .check_crc = true, .data = dir, .slot = state.slot, }), arg); _ = try waitEvents(Event.cmd_done, Event.command_errors, command_done_timeout_us); _ = try checkR5(resp0.raw()); _ = try waitEvents(Event.dto, Event.data_errors, data_done_timeout_us); } // -------------------------------------------------------------------- SDIO card interrupt (D1) /// Has the card asserted its interrupt line? /// /// Non-blocking, no side effect, straight out of RINTSTS bit 16+slot (`sdmmc_reg.h:621-631`). It /// does **not** clear the bit; `clearSlaveInterrupt` does, deliberately, once a caller has decided /// to act on it. /// /// Two different trigger behaviours meet at this bit and it is worth keeping them apart, because /// conflating them sends you tuning the wrong knob: /// /// * **Card -> controller is an edge.** ESP-IDF: "SDIO interrupts are negedge sensitive ones: /// the status bit is only set when first interrupt triggered" (`sd_host_sdmmc.c:396-402`). /// That is why a waiter must check D1's level once before sleeping - an edge that arrived /// while it was awake is not re-delivered. /// * **Controller -> CLIC is a level.** RINTSTS is a sticky write-1-to-clear latch, so the /// controller's output line stays asserted until software clears the bit that raised it. The /// CLIC line therefore wants `.level`, and an edge trigger there would only hide a handler /// that fails to deassert rather than fix it. /// /// This is the polling half. The interrupt half is a CLIC line and belongs to whoever owns the /// scheduler: route `interrupt_source` with `hal.intr`, and in the handler mask the bit /// (`setSlaveInterruptEnabled(false)`) before waking anybody. ESP-IDF does exactly that /// (`sd_host_sdmmc.c:826-830`) and explains why at `:396-402`: "SDIO interrupts are negedge /// sensitive ones: the status bit is only set when first interrupt triggered", so a handler that /// leaves the bit unmasked and unhandled re-enters forever, and a waiter that sleeps without first /// checking D1's level loses an edge that arrived while it was awake. pub fn slaveInterruptPending() bool { return rintsts.raw() & (Event.io_slot0 << state.slot) != 0; } /// The raw masked-interrupt status word. Diagnostics only: a hang waiting on the card interrupt is /// otherwise indistinguishable from a card that never asserted, and this is the register that tells /// them apart. pub fn interruptStatusRaw() u32 { return rintsts.raw(); } pub fn clearSlaveInterrupt() void { rintsts.writeRaw(Event.io_slot0 << state.slot); } /// INTMASK as written. The other half of "why is this line asserted": the controller's output is /// RINTSTS AND INTMASK, and a diagnostic that prints only RINTSTS shows half the conjunction. /// /// **Not evidence about an arm.** INTMASK is an ordinary read/write register, so this returns /// whatever the last store left - and on the interrupt path the last store is usually the *disarm*. /// A caller that wants to know whether unmasking took effect must read the register back inside the /// same masked region as the store; that is `armSlaveInterrupt`, and it exists because this /// function was read as if it answered that question and it never could. pub fn interruptMaskRaw() u32 { return intmask.raw(); } /// IDSTS - the IDMAC's own status word, which reaches the controller's interrupt output through /// IDINTEN and *not* through INTMASK. /// /// The second independent reason the line can be asserted, and therefore the first thing to read /// when a handler entry cannot be explained by RINTSTS. `muteInterrupts` leaves IDINTEN at zero so /// this cannot raise the line in this driver; a foreign handler entry with bits set here means /// something put IDINTEN back. pub fn dmaStatusRaw() u32 { return idsts.raw(); } /// Clear every latched event *except* this slot's SDIO card interrupt. /// /// For an interrupt handler that has to lower the controller's output line without racing the /// card: the card interrupt is the one event the handler is being woken for, and clearing it here /// would drop the wakeup. Everything else - a stale command-done, a latched card-detect, an error /// from a transfer that has already been reported - is safe to drop on the floor, and leaving any /// of it latched while unmasked keeps the CLIC line high. pub fn clearNonSlaveInterrupts() void { rintsts.writeRaw(0x0003_ffff & ~(Event.io_slot0 << state.slot)); } /// Unmask this slot's SDIO card interrupt, i.e. let it - and after `muteInterrupts`, *only* it - /// reach the CLIC. RINTSTS records the event either way, so polling works without this. /// /// This is the whole of the masking half of arming a waiter: `muteInterrupts` has already left /// every other bit of INTMASK and all of IDINTEN at zero, so `true` here makes this slot's card /// interrupt the single reason the controller's output can assert - which is what a /// level-triggered CLIC line with a one-cause handler requires. /// /// The *order* around it is the part that is easy to get wrong, and it belongs to whoever owns the /// scheduler rather than here. `sd_host_slot_sdmmc_io_int_wait` (`sd_host_sdmmc.c:404-426`) is the /// reference, and it is four steps: /// /// 1. `setSlaveInterruptEnabled(false)` - mask, so nothing arrives while the state is in flux. /// 2. `clearSlaveInterrupt()` - drop the latched edge, so a stale one is not delivered as news. /// 3. `slaveInterruptAsserted()` - **if true, act now and do not sleep.** The capture is a /// negedge, so with D1 already low step 2 has just thrown away the only edge there will be. /// 4. `setSlaveInterruptEnabled(true)` - unmask, and not before. Nothing can be lost between 2 /// and 4: D1 is a level, and unmasking a bit RINTSTS has already latched asserts the line at /// once. /// /// A handler on that line must mask again as its **unconditional first act**, on every path /// including the one where the cause turns out not to be its own. The line is a level and it does /// not lower itself. pub fn setSlaveInterruptEnabled(on: bool) void { // Masked, and that is not decoration. `sdioDispatch` performs *this same* read-modify-write on // *this same* two-bit field, from an interrupt handler, as its unconditional first act. A task // interrupted between the load and the store puts back the bit the handler had just cleared - // re-arming a level-triggered line with nobody left waiting on it, which is precisely how the // storm gets its second chance. Two CSR instructions, and `clkrst.Guard` composes: called from // inside a handler, where MIE is already clear, it leaves MIE clear. // // The register writes are unchanged, so the `sdio_interrupt` differential case still compares // the same resulting word against `sdmmc_ll_enable_sdio_interrupt`'s. const guard = intr.mask(); defer guard.release(); const m = slotBit(); const cur = intmask.get(sdio_int_mask); intmask.modify(.{sdio_int_mask.is(if (on) cur | m else cur & ~m)}); } /// What the controller reported the instant after this slot's card interrupt was unmasked. pub const Armed = struct { /// The bit the store was trying to set, i.e. `slaveInterruptMask()`. want: u32, /// INTMASK, read back inside the same masked region as the store. intmask: u32, /// MINTSTS - `RINTSTS & INTMASK`, and the only word the controller's output follows. Zero here /// with `stuck()` true is the normal way to enter a sleep: the mask took and nothing is latched /// yet. mintsts: u32, /// RINTSTS, for the case where the edge landed between the unmask and the read-back. rintsts: u32, /// Did the unmask take effect? pub inline fn stuck(self: Armed) bool { return self.intmask & self.want != 0; } }; /// Unmask this slot's card interrupt and read the result back, both inside one masked region. /// /// This exists because the opposite conclusion was drawn from diagnostics that could not support /// it. Every arming window on this board printed `intmask=0x00000000` and that was read as "the /// unmask does not stick" - but `MARK PORT_SDIO_LAPSE` prints *after* the disarm, which had just /// written that zero deliberately, and `interruptDiagnostics` runs on the application task, which /// is never inside an arming window. Neither reading could ever have shown anything else, whatever /// the hardware did. /// /// So the claim gets an instrument instead of an argument. Nothing runs between the store and the /// three loads: no task, because the runtime is cooperative, and no handler, because MIE is clear. /// A `stuck()` of false here is a fact about this register on this die; `stuck()` true retires the /// hypothesis. pub fn armSlaveInterrupt() Armed { const guard = intr.mask(); defer guard.release(); const m = slotBit(); intmask.modify(.{sdio_int_mask.is(intmask.get(sdio_int_mask) | m)}); return .{ .want = slaveInterruptMask(), .intmask = intmask.raw(), .mintsts = mintsts.raw(), .rintsts = rintsts.raw(), }; } /// This slot's bit in RINTSTS/INTMASK/MINTSTS - the only interrupt cause a waiter here understands. pub fn slaveInterruptMask() u32 { return Event.io_slot0 << state.slot; } /// Is the card asserting its interrupt *right now*? /// /// Read from D1's pad rather than from RINTSTS, because the two answer different questions: the /// register says "a negedge was latched and not yet cleared", the pad says "the card is holding the /// line low". Only the second is safe to test before sleeping, and it is what ESP-IDF tests - /// `gpio_get_level(slot_ctx->io_config.d1_io) == 0` at `sd_host_sdmmc.c:413-415`. /// /// Requires D1's input buffer and matrix input to be configured, which `configurePins` does. pub fn slaveInterruptAsserted() bool { return gpio.getLevel(state.pins.d1) == 0; } /// The masked status word - what the controller's interrupt output is actually looking at. /// /// Non-zero here and a silent CLIC means the delivery path above the controller is broken (source /// routing, line enable, priority, threshold, mstatus.MIE). Zero here while `interruptStatusRaw` /// is non-zero means the event is latched but masked, which is the normal resting state of this /// driver. pub fn interruptStatusMasked() u32 { return mintsts.raw(); } // ------------------------------------------------------------------------------- diagnostics /// `ets_printf` from the mask ROM, the same declaration `src/net/port.zig:75` makes and for the /// same reason: this file's only module imports are `regs`, `mmio` and its sibling HALs, and the /// symbol comes from the generated linker script rather than from any of them. extern fn ets_printf(fmt: [*:0]const u8, ...) c_int; fn note(comptime fmt: [*:0]const u8, args: anytype) void { _ = @call(.auto, ets_printf, .{fmt} ++ args); } inline fn yesno(b: bool) u32 { return @intFromBool(b); } /// Print the whole card-interrupt delivery chain, in the order a signal traverses it, so that one /// flash says which link is broken. Reads registers only: no loop, no wait, no side effect on any /// of the state it reports. /// /// The chain has four links and each line below covers one: /// /// * `SDIO_DIAG_PAD` - the card's end. `asserted=1` means D1 is low, i.e. the C6 is requesting /// service at this instant. `ie=0` or `in_src` not equal to D1's pad number means the /// controller cannot see D1 at all, and no amount of unmasking will help. /// * `SDIO_DIAG_TIEOFF` - the two matrix inputs with no pin on this board. `card_int_n` must read /// 63 (constant one, inactive) and `card_detect_n` 62 (constant zero, card present), both with /// `from_matrix=1`. A `card_int_n` stuck at a constant *zero* is an interrupt that is asserted /// before software ever runs, so the first negedge happens before anyone is watching and no /// second one ever comes. /// * `SDIO_DIAG_CTLR` - the controller's end. `latched` is RINTSTS's bit for this slot, /// `unmasked` is INTMASK's, and `mintsts` is the conjunction the interrupt output follows. /// `idsts`/`idinten` are the other, independent reason this output can be asserted. /// /// **`unmasked=0` here is the resting state and is not a finding.** This function is called /// from an application task; the card interrupt is unmasked only inside an arming window, on /// the transport's own task, and is masked again by the handler or the disarm before that task /// yields. So an application can never observe the mask up, whatever the hardware does, and /// reading a zero here as "the unmask does not stick" is what cost this path a week. The /// register read that can answer that question is `armSlaveInterrupt`. /// * `SDIO_DIAG_CLIC` - delivery. An unrouted source, a clear `enabled`, a priority at or below /// `thresh`, or `mie=0` each mean the line exists and cannot arrive. pub fn interruptDiagnostics() void { const m = slaveInterruptMask(); const rsts = rintsts.raw(); const imask = intmask.raw(); const d1 = state.pins.d1; const d1_in = gpio.matrixInSource(sig.cdata1); const ci = gpio.matrixInSource(sig.card_int); const cdet = gpio.matrixInSource(sig.card_detect); note("MARK SDIO_DIAG_PAD d1=gpio%u level=%u asserted=%u ie=%u in_src=%u from_matrix=%u inv=%u expect in_src=%u\r\n", .{ @as(u32, d1), @as(u32, gpio.getLevel(d1)), yesno(slaveInterruptAsserted()), yesno(gpio.isInputEnabled(d1)), @as(u32, d1_in.pin), yesno(d1_in.from_matrix), yesno(d1_in.inverted), @as(u32, d1), }); note("MARK SDIO_DIAG_TIEOFF card_int_n=%u/%u card_detect_n=%u/%u expect 63/1 and 62/1\r\n", .{ @as(u32, ci.pin), yesno(ci.from_matrix), @as(u32, cdet.pin), yesno(cdet.from_matrix), }); note("MARK SDIO_DIAG_CTLR slot=%u bit=0x%05x latched=%u unmasked=%u rintsts=0x%08x intmask=0x%08x mintsts=0x%08x\r\n", .{ @as(u32, state.slot), m, yesno(rsts & m != 0), yesno(imask & m != 0), rsts, imask, interruptStatusMasked(), }); note("MARK SDIO_DIAG_CTRL ctrl=0x%08x int_enable=%u idsts=0x%08x idinten=0x%08x status=0x%08x clkena=0x%08x\r\n", .{ ctrl.raw(), ctrl.get(int_enable), idsts.raw(), idinten.raw(), status.raw(), clkena.raw(), }); const src: u32 = @intFromEnum(interrupt_source); if (intr.routedLine(interrupt_source)) |line| { note("MARK SDIO_DIAG_CLIC source=%u line=%u enabled=%u pending=%u trigger=%u prio=%u thresh=%u mie=%u\r\n", .{ src, @as(u32, line), yesno(intr.isEnabled(line)), yesno(intr.isPending(line)), @as(u32, @intFromEnum(intr.getTrigger(line))), @as(u32, intr.getPriority(line)), @as(u32, intr.getThreshold()), yesno(intr.globalEnabled()), }); } else { note("MARK SDIO_DIAG_CLIC source=%u UNROUTED - no CLIC line can deliver this interrupt\r\n", .{src}); } } // --------------------------------------------------------------------------------- host tests // // Everything below runs on the host under `zig build test`. It covers the two things in this file // that are pure functions of their arguments - the command word and the CMD52/CMD53 argument // layouts - plus the divider table and the chunking rule. The register sequences are not testable // here; that is what src/oracle/sdmmc_cases.zig is for. const testing = std.testing; test "CMD52 read is the word ESP-IDF builds" { // make_hw_cmd for {opcode 52, SCF_CMD_AC | SCF_RSP_R5}: response_expect (R5 is PRESENT), // check_response_crc (R5 has CRC), wait_complete (not CMD0/12/11), no data. Then // sd_host_slot_start_command adds use_hold_reg, card_num and start_command. const w = commandWord(.{ .index = 52, .response = .short, .check_crc = true, .slot = 1 }); try testing.expectEqual(@as(u32, 0xA001_2174), w); } test "CMD52 on slot 0 differs from slot 1 only in card_num" { const s0 = commandWord(.{ .index = 52, .response = .short, .check_crc = true, .slot = 0 }); const s1 = commandWord(.{ .index = 52, .response = .short, .check_crc = true, .slot = 1 }); try testing.expectEqual(@as(u32, 0xA000_2174), s0); try testing.expectEqual(@as(u32, 1 << 16), s0 ^ s1); } test "CMD53 sets data_expected, and rw only when writing" { const rd = commandWord(.{ .index = 53, .response = .short, .check_crc = true, .data = .read, .slot = 1 }); const wr = commandWord(.{ .index = 53, .response = .short, .check_crc = true, .data = .write, .slot = 1 }); try testing.expectEqual(@as(u32, 0xA001_2375), rd); try testing.expectEqual(@as(u32, 0xA001_2775), wr); try testing.expectEqual(@as(u32, 1 << 10), rd ^ wr); } test "CMD0 sends the init sequence and expects nothing back" { // The only command make_hw_cmd gives send_init and denies wait_complete. const w = commandWord(.{ .index = 0, .send_init = true, .wait_prvdata = false, .slot = 1 }); try testing.expectEqual(@as(u32, 0xA001_8000), w); try testing.expectEqual(@as(u32, 0), w & (1 << 6)); // no response expected } test "CMD5's response CRC is not checked" { // R4 is SCF_RSP_PRESENT alone (sd_protocol_types.h:141) - the OCR response carries no valid // CRC7, and checking it would fail every card. const w = commandWord(.{ .index = 5, .response = .short, .check_crc = false, .slot = 1 }); try testing.expectEqual(@as(u32, 0xA001_2045), w); try testing.expectEqual(@as(u32, 0), w & (1 << 8)); } test "CMD3 and CMD7 do check it" { try testing.expectEqual( @as(u32, 0xA001_2143), commandWord(.{ .index = 3, .response = .short, .check_crc = true, .slot = 1 }), ); try testing.expectEqual( @as(u32, 0xA001_2147), commandWord(.{ .index = 7, .response = .short, .check_crc = true, .slot = 1 }), ); } test "the clock update command sends nothing to the card" { const w = commandWord(.{ .index = 0, .update_clock = true, .slot = 1 }); try testing.expectEqual(@as(u32, 0xA021_2000), w); try testing.expectEqual(@as(u32, 1 << 21), w & (1 << 21)); try testing.expectEqual(@as(u32, 0), w & (1 << 6)); } test "a long response sets response_length as well as response_expect" { const w = commandWord(.{ .index = 2, .response = .long, .check_crc = true, .slot = 1 }); try testing.expectEqual(@as(u32, 1 << 7), w & (1 << 7)); try testing.expectEqual(@as(u32, 1 << 6), w & (1 << 6)); } test "every command word starts the command and uses the hold register" { for ([_]Command{ .{ .index = 52, .response = .short, .check_crc = true }, .{ .index = 53, .response = .short, .check_crc = true, .data = .read }, .{ .index = 0, .send_init = true, .wait_prvdata = false }, }) |c| { const w = commandWord(c); try testing.expect(w & (1 << 31) != 0); try testing.expect(w & (1 << 29) != 0); } } test "CMD52 argument layout" { // Read CCCR 0x00 on function 0: everything zero. try testing.expectEqual(@as(u32, 0), cmd52Arg(false, 0, 0x00, false, 0)); // Write 0x02 to CCCR 0x02 (I/O enable) on function 0. try testing.expectEqual(@as(u32, 0x8000_0402), cmd52Arg(true, 0, 0x02, false, 0x02)); // Function 1, address 0x1F800, data 0xAB, with the read-after-write flag. const a = cmd52Arg(true, 1, 0x1F800, true, 0xAB); try testing.expectEqual(@as(u32, 1), a >> 31); try testing.expectEqual(@as(u32, 1), (a >> 28) & 0x7); try testing.expectEqual(@as(u32, 1), (a >> 27) & 1); try testing.expectEqual(@as(u32, 0x1F800), (a >> 9) & 0x1FFFF); try testing.expectEqual(@as(u32, 0xAB), a & 0xFF); } test "CMD53 argument layout, both modes" { // Block mode, function 1, address 0, incrementing, one block. const blk = cmd53Arg(false, 1, 0, true, true, 1); try testing.expectEqual(@as(u32, 0x1C00_0001), blk); // Byte mode, function 1, fixed address, 12 bytes - the ESP-Hosted length read. const byt = cmd53Arg(false, 1, 0x058, false, false, 12); try testing.expectEqual(@as(u32, 0x1000_B00C), byt); // Writing sets bit 31 and nothing else. try testing.expectEqual( @as(u32, 1) << 31, cmd53Arg(true, 1, 0x058, false, false, 12) ^ byt, ); } test "byte mode encodes 512 as a count of zero" { // SDIO simplified spec 5.3.1, as applied at sdmmc_io.c:351-355. The chunker never produces // this case - a 512-byte request is a whole block and goes block mode - so the encoder is // checked directly. ESP-Hosted can still reach it: a 512-byte read at a fixed address. try testing.expectEqual(@as(u32, 0), cmd53Arg(false, 1, 0, false, true, 0) & 0x1ff); const c = nextChunk(512); try testing.expect(c.block_mode); try testing.expectEqual(@as(u32, 512), c.len); try testing.expectEqual(@as(u9, 1), c.count); } test "chunking splits on block boundaries and clamps to the bounce buffer" { // Whole blocks, within the buffer: one block-mode command. try testing.expectEqual(@as(u32, 1024), nextChunk(1024).len); try testing.expect(nextChunk(1024).block_mode); // More blocks than fit: clamped to bounce_len, still block mode, still whole blocks. const big = nextChunk(8192); try testing.expect(big.block_mode); try testing.expectEqual(bounce_len, big.len); try testing.expectEqual(@as(u9, bounce_len / 512), big.count); // Not a block multiple: byte mode, count in bytes. const odd = nextChunk(12); try testing.expect(!odd.block_mode); try testing.expectEqual(@as(u32, 12), odd.len); try testing.expectEqual(@as(u9, 12), odd.count); // Longer than one byte-mode command can carry: clamped to 512. const long = nextChunk(1000); try testing.expect(!long.block_mode); try testing.expectEqual(@as(u32, 512), long.len); } test "the controller's 4-byte rule turns a 6-byte transfer into 4 then 2" { // sd_trans_sdmmc.c:526-532 rejects a length that is >= 4 and not a multiple of 4 outright. const first = nextChunk(6); try testing.expect(!first.block_mode); try testing.expectEqual(@as(u32, 4), first.len); const second = nextChunk(6 - first.len); try testing.expectEqual(@as(u32, 2), second.len); // Under four bytes the whole thing goes in one command; that is the case the rule exempts. try testing.expectEqual(@as(u32, 3), nextChunk(3).len); try testing.expectEqual(@as(u32, 1), nextChunk(1).len); // And every chunk a loop produces is either aligned or a final short tail. var remaining: u32 = 1023; var commands: u32 = 0; while (remaining > 0) { const c = nextChunk(remaining); try testing.expect(c.len > 0); try testing.expect(c.len < 4 or c.len % 4 == 0); remaining -= c.len; commands += 1; try testing.expect(commands < 8); // 512 + 508 + 3, not an unbounded walk } } test "divider table reproduces ESP-IDF's three named frequencies" { try testing.expectEqual(Dividers{ .host = 10, .card = 20 }, dividersFor(400)); try testing.expectEqual(Dividers{ .host = 8, .card = 0 }, dividersFor(20_000)); try testing.expectEqual(Dividers{ .host = 4, .card = 0 }, dividersFor(40_000)); } test "divider table lands on or below the requested frequency" { for ([_]u32{ 400, 1_000, 5_000, 10_000, 20_000, 25_000, 40_000 }) |khz| { const d = dividersFor(khz); const div: u64 = @as(u64, d.host) * (if (d.card == 0) @as(u64, 1) else @as(u64, d.card) * 2); const actual_khz = 160_000 / div; try testing.expect(actual_khz <= khz); } } test "the DMA region is one aligned block of exactly the documented size" { try testing.expectEqual(@as(usize, 16), @sizeOf(Descriptor)); try testing.expectEqual(@as(usize, 64 + bounce_len), @sizeOf(DmaRegion)); try testing.expectEqual(@as(usize, 0), @offsetOf(DmaRegion, "desc")); try testing.expectEqual(@as(usize, 64), @offsetOf(DmaRegion, "buf")); // Both halves of what the one-shot cache maintenance call needs: a base on a cache line and a // length that is a whole number of them. The alignment is on the variable, not on the type - // `@alignOf(DmaRegion)` is 4 - so it has to be checked on the object. try testing.expectEqual(@as(usize, 0), @intFromPtr(&dma) % cache_line); try testing.expectEqual(@as(usize, 0), @sizeOf(DmaRegion) % cache_line); } test "the non-cacheable alias is a fixed offset and nothing more" { var cell: u32 = 0; const a = cachedAddr(&cell); try testing.expectEqual(a +% @as(usize, 0x4000_0000), uncachedAddr(&cell)); // And it is the offset ESP-IDF uses, not one this file invented. try testing.expectEqual(@as(u32, 0x4000_0000), non_cacheable_offset); } test "descriptor flags are the bits sdmmc_struct.h names" { try testing.expectEqual(@as(u32, 1 << 2), Descriptor.last_descriptor); try testing.expectEqual(@as(u32, 1 << 3), Descriptor.first_descriptor); try testing.expectEqual(@as(u32, 1 << 4), Descriptor.second_address_chained); try testing.expectEqual(@as(u32, 1 << 31), Descriptor.owned_by_idmac); // The word a single-descriptor transfer writes. const flags = Descriptor.owned_by_idmac | Descriptor.first_descriptor | Descriptor.last_descriptor | Descriptor.second_address_chained; try testing.expectEqual(@as(u32, 0x8000_001C), flags); } test "the default interrupt mask is ESP-IDF's SDMMC_LL_EVENT_DEFAULT" { // sdmmc_ll.h:64-69, expanded: CD|RESP_ERR|CMD_DONE|DATA_OVER|RCRC|DCRC|RTO|DTO|HTO|HLE|SBE|EBE try testing.expectEqual(@as(u32, 0xB7CF), Event.default); // and it deliberately excludes the two per-FIFO-word requests and both SDIO card interrupts. try testing.expectEqual(@as(u32, 0), Event.default & (Event.txdr | Event.rxdr)); try testing.expectEqual(@as(u32, 0), Event.default & (Event.io_slot0 | Event.io_slot1)); } test "the armed mask drops card detect, and nothing else" { // The bit that produced an unstoppable CLIC line 21: cd latches during pin setup, nothing in // the command path clears it, and while it is unmasked the controller's output never // deasserts. `configureInterrupts` writes `armed`, not `default`. try testing.expectEqual(@as(u32, 0xB7CE), Event.armed); try testing.expectEqual(@as(u32, 0), Event.armed & Event.cd); try testing.expectEqual(Event.cd, Event.default ^ Event.armed); // Every event a transfer actually waits on survives the change. for ([_]u32{ Event.cmd_done, Event.dto, Event.re, Event.rcrc, Event.dcrc, Event.rto, Event.drto, Event.hto, Event.hle, Event.sbe, Event.ebe }) |e| { try testing.expect(Event.armed & e != 0); } }