//! Does CMD53 return the same bytes as CMD52? //! //! Written to settle one question during radio bring-up. The transport gets all the way to "Open //! data path at slave" and then never sees the slave's NEW_PACKET bit, which it learns by reading the //! slave's interrupt register through `sdio_read_regs` - a multi-byte read, so CMD53. CMD52 is //! already proven on this board: the CCCR write-and-read-back during card init cannot succeed //! without it. CMD53 is not, and it is the one that uses the IDMAC and therefore the one exposed to //! every DMA and cache mistake. //! //! The test is a differential against the bus itself. Read the same slave register window twice - //! once with a single CMD53, once byte at a time with CMD52 - and compare. Both go to the same //! addresses on the same card in the same boot, so a disagreement is our CMD53 and nothing else. //! //! Registers are the ones the transport actually reads, from //! host/drivers/transport/sdio/sdio_reg.h, masked with ESP_ADDRESS_MASK (0x3FF) exactly as //! `hosted_sdio_read_reg` does at port_esp_hosted_host_sdio.c:500: //! //! 0x050 ESP_SLAVE_INT_RAW_REG bit 23 is RX_NEW_PACKET, the bit the read task waits for //! 0x058 ESP_SLAVE_INT_ST_REG //! 0x060 ESP_SLAVE_PACKET_LEN_REG the slave's cumulative TX length counter //! 0x044 ESP_SLAVE_TOKEN_RDATA the slave's receive-buffer credit //! 0x06C ESP_SLAVE_SCRATCH_REG_0 written by the coprocessor firmware //! //! What the output means: //! //! MARK SDIO_CMP ... SAME CMD53 agrees with CMD52 - the data path is good and the missing //! NEW_PACKET is the slave's silence, not our bus //! MARK SDIO_CMP ... DIFFER CMD53 is broken; the bytes printed say how (all-zero is a DMA that //! never landed, stale is a cache view, shifted is an address or //! length error) //! MARK SDIO_LEN ... the slave's packet-length counter, read twice a second apart. If it //! moves, the C6 is queueing data for us and the fault is on the host //! side of the interrupt. If it never moves, the C6 has nothing to say. //! //! Run: zig build -Dapp=examples/sdiocheck.zig run -Dseconds=15 const std = @import("std"); const soc = @import("soc"); const hal = @import("hal"); pub const panic = std.debug.FullPanic(struct { fn call(msg: []const u8, _: ?usize) noreturn { soc.rom.print("MARK SDIO_PANIC %s\r\n", .{msg.ptr}); while (true) {} } }.call); /// FN1: every slave register lives in function 1 (port_esp_hosted_host_sdio.c:505). const func: u3 = 1; /// The windows to compare. Length 4 for the 32-bit registers, and one longer run to catch a length /// or block-boundary error that a 4-byte read would not. const windows = [_]struct { name: [*:0]const u8, addr: u17, len: usize }{ .{ .name = "INT_RAW 0x050", .addr = 0x050, .len = 4 }, .{ .name = "INT_ST 0x058", .addr = 0x058, .len = 4 }, .{ .name = "PKT_LEN 0x060", .addr = 0x060, .len = 4 }, .{ .name = "TOKEN 0x044", .addr = 0x044, .len = 4 }, .{ .name = "SCRATCH 0x06C", .addr = 0x06C, .len = 4 }, .{ .name = "RUN 0x050", .addr = 0x050, .len = 16 }, }; export fn zig_main() noreturn { _ = hal.rwdt.disable(); soc.rom.print("\r\nMARK SDIO_START\r\n", .{}); // The C6 must be out of reset and booted before it will answer anything. Active low with an // external pull-up: drive low to hold, release to run. Driving it high would fight the pull-up. hal.gpio.configureOutput(54, .{}); hal.gpio.setLow(54); soc.rom.ets_delay_us(20_000); hal.gpio.outputDisable(54); // release; the pull-up takes it high soc.rom.print("MARK SDIO_RESET released gpio54, waiting for the C6 to boot\r\n", .{}); soc.rom.ets_delay_us(1_500_000); hal.sdmmc.init(.{ .slot = 1, .width = .four, .khz = 40_000 }) catch |err| { soc.rom.print("MARK SDIO_FAIL init=%s\r\n", .{@errorName(err).ptr}); park(); }; hal.sdmmc.cardInit() catch |err| { soc.rom.print("MARK SDIO_FAIL cardInit=%s\r\n", .{@errorName(err).ptr}); park(); }; soc.rom.print("MARK SDIO_CARD up\r\n", .{}); // FN1 must be enabled and ready before its registers answer, exactly as card init does for the // transport. Without this the reads below are against a disabled function and return zeros - // which would look identical to a broken CMD53, so it is done explicitly rather than assumed. enableFn1() catch |err| { soc.rom.print("MARK SDIO_FAIL fn1=%s\r\n", .{@errorName(err).ptr}); park(); }; var buf53: [32]u8 = undefined; var buf52: [32]u8 = undefined; var differ: u32 = 0; for (windows) |w| { // CMD53 first, then CMD52, then CMD53 again. The third read is what tells a genuine // disagreement apart from a register that simply changed between the two reads - these are // live status registers, and a differing INT_RAW could be honest. hal.sdmmc.cmd53Read(func, w.addr, buf53[0..w.len], true) catch |err| { soc.rom.print("MARK SDIO_CMP %s cmd53=%s\r\n", .{ w.name, @errorName(err).ptr }); differ += 1; continue; }; for (0..w.len) |i| { buf52[i] = hal.sdmmc.cmd52Read(func, @intCast(w.addr + i)) catch { soc.rom.print("MARK SDIO_CMP %s cmd52 failed at +%u\r\n", .{ w.name, @as(u32, @intCast(i)) }); differ += 1; break; }; } const same = std.mem.eql(u8, buf53[0..w.len], buf52[0..w.len]); if (!same) differ += 1; soc.rom.print("MARK SDIO_CMP %s len=%u cmd53=%s cmd52=%s %s\r\n", .{ w.name, @as(u32, @intCast(w.len)), hex(&buf53, w.len, &hexbuf_a), hex(&buf52, w.len, &hexbuf_b), @as([*:0]const u8, if (same) "SAME" else "DIFFER"), }); } soc.rom.print("MARK SDIO_CMP_TOTAL windows=%u differing=%u\r\n", .{ @as(u32, windows.len), differ, }); // Is the slave producing anything at all? PACKET_LEN is a cumulative counter of bytes the slave // has made available. Sampled twice a second apart: movement means the C6 is queueing data and // the fault is on our side of the interrupt; no movement means it has nothing to send. var a: [4]u8 = undefined; var b: [4]u8 = undefined; hal.sdmmc.cmd53Read(func, 0x060, &a, true) catch {}; soc.rom.ets_delay_us(1_000_000); hal.sdmmc.cmd53Read(func, 0x060, &b, true) catch {}; const len_a = std.mem.readInt(u32, &a, .little) & 0xFFFFF; const len_b = std.mem.readInt(u32, &b, .little) & 0xFFFFF; soc.rom.print("MARK SDIO_LEN first=%u second=%u moved=%u\r\n", .{ len_a, len_b, @as(u32, @intFromBool(len_a != len_b)), }); // And the interrupt register, decoded, because bit 23 is the whole question. var ir: [4]u8 = undefined; hal.sdmmc.cmd53Read(func, 0x050, &ir, true) catch {}; const raw = std.mem.readInt(u32, &ir, .little); soc.rom.print("MARK SDIO_INTRAW 0x%08x new_packet=%u\r\n", .{ raw, @as(u32, @intFromBool(raw & (1 << 23) != 0)), }); // Every scratch register, because this is where the coprocessor firmware announces itself. All // zeroes would say the C6 is answering as a card but not running ESP-Hosted's slave app. var scratch: [8]u32 = undefined; const scratch_addr = [_]u17{ 0x06C, 0x070, 0x074, 0x078, 0x07C, 0x080, 0x088, 0x08C }; for (scratch_addr, 0..) |a2, i| { var w: [4]u8 = undefined; hal.sdmmc.cmd53Read(func, a2, &w, true) catch {}; scratch[i] = std.mem.readInt(u32, &w, .little); } soc.rom.print("MARK SDIO_SCRATCH %08x %08x %08x %08x %08x %08x %08x %08x\r\n", .{ scratch[0], scratch[1], scratch[2], scratch[3], scratch[4], scratch[5], scratch[6], scratch[7], }); // The poke the transport makes after card init: ESP_OPEN_DATA_PATH is enum value 0, so // sdio_generate_slave_intr writes BIT(0 + ESP_SDIO_CONF_OFFSET) = 0x01 to // HOST_TO_SLAVE_INTR = ESP_SLAVE_SCRATCH_REG_7, masked to offset 0x08C // (sdio_drv.c:404-415, sdio_reg.h:49,77,99). // // This is the decisive test. If the slave answers a poke with a packet, our host's read loop is // at fault. If it stays silent, the coprocessor is not responding to the protocol and no amount // of host-side work will help. soc.rom.print("MARK SDIO_POKE writing 0x01 to 0x08c (ESP_OPEN_DATA_PATH)\r\n", .{}); hal.sdmmc.cmd52Write(func, 0x08C, 0x01) catch |err| { soc.rom.print("MARK SDIO_POKE write failed=%s\r\n", .{@errorName(err).ptr}); }; var beat: u32 = 0; while (beat < 20) : (beat += 1) { soc.rom.ets_delay_us(100_000); var w1: [4]u8 = undefined; var w2: [4]u8 = undefined; hal.sdmmc.cmd53Read(func, 0x050, &w1, true) catch {}; hal.sdmmc.cmd53Read(func, 0x060, &w2, true) catch {}; const iraw = std.mem.readInt(u32, &w1, .little); const plen = std.mem.readInt(u32, &w2, .little) & 0xFFFFF; if (iraw & (1 << 23) != 0 or plen != 0) { soc.rom.print("MARK SDIO_ANSWER beat=%u int_raw=0x%08x new_packet=1 pkt_len=%u\r\n", .{ beat, iraw, plen, }); break; } if (beat % 5 == 0) { soc.rom.print("MARK SDIO_WAIT beat=%u int_raw=0x%08x pkt_len=%u\r\n", .{ beat, iraw, plen }); } } if (beat >= 20) soc.rom.print("MARK SDIO_SILENT no packet 2 s after the poke\r\n", .{}); // CMD53 WRITES, which nothing has yet verified. // // The reads above are proven identical to CMD52. Writes are the other half and they are the // half the transport depends on: every RPC request leaves through a CMD53 write, and a request // that arrives corrupted at the slave produces exactly what the board shows - the request goes // out, the coprocessor makes nothing of it, and no response ever comes back. // // Writes are also where the cache hazard points the other way. On a read, DMA fills memory and // the CPU must not see a stale cached line. On a write, the CPU fills a buffer - which lands in // the L1 D-cache - and DMA reads from memory, so without a write-back the controller sends // whatever was in memory before. Reads working tells us nothing about writes. // // Scratch register 1 (offset 0x070) is the target: writable from the host, and not the one that // triggers slave interrupts (that is register 7 at 0x08C, poked above). const scratch1: u17 = 0x070; const patterns = [_][4]u8{ .{ 0xde, 0xad, 0xbe, 0xef }, .{ 0x00, 0x00, 0x00, 0x00 }, .{ 0xff, 0xff, 0xff, 0xff }, .{ 0x42, 0x00, 0x42, 0x00 }, }; var write_bad: u32 = 0; for (patterns) |pat| { var out = pat; // a mutable copy, so the driver may not rely on the source being static hal.sdmmc.cmd53Write(func, scratch1, &out, true) catch |err| { soc.rom.print("MARK SDIO_W53 pattern=%s write failed=%s\r\n", .{ hex(&pat, 4, &hexbuf_a), @errorName(err).ptr, }); write_bad += 1; continue; }; // Read back with CMD52, the path already proven, so a mismatch can only be the write. var back: [4]u8 = undefined; for (0..4) |i| { back[i] = hal.sdmmc.cmd52Read(func, @intCast(scratch1 + i)) catch 0xAA; } const ok = std.mem.eql(u8, &pat, &back); if (!ok) write_bad += 1; soc.rom.print("MARK SDIO_W53 wrote=%s read=%s %s\r\n", .{ hex(&pat, 4, &hexbuf_a), hex(&back, 4, &hexbuf_b), @as([*:0]const u8, if (ok) "SAME" else "DIFFER"), }); } // And the same patterns through CMD52 writes, as the control: if these also fail the register is // not writable and the CMD53 result above means nothing. var ctrl_bad: u32 = 0; for (patterns) |pat| { for (0..4) |i| { hal.sdmmc.cmd52Write(func, @intCast(scratch1 + i), pat[i]) catch {}; } var back: [4]u8 = undefined; for (0..4) |i| { back[i] = hal.sdmmc.cmd52Read(func, @intCast(scratch1 + i)) catch 0xAA; } if (!std.mem.eql(u8, &pat, &back)) ctrl_bad += 1; } soc.rom.print("MARK SDIO_W_TOTAL cmd53_bad=%u cmd52_bad=%u of %u\r\n", .{ write_bad, ctrl_bad, @as(u32, patterns.len), }); soc.rom.print("MARK SDIO_DONE\r\n", .{}); park(); } /// Enable function 1 and wait for it to report ready, then set its block size. The CCCR offsets are /// the standard SDIO ones; the sequence mirrors what src/net/port.zig's card init does, kept local so /// this example does not depend on the radio stack being built. fn enableFn1() !void { const cccr_fn_enable = 0x02; const cccr_fn_ready = 0x03; const fn1: u8 = 1 << 1; const ioe = try hal.sdmmc.cmd52Read(0, cccr_fn_enable); try hal.sdmmc.cmd52Write(0, cccr_fn_enable, ioe | fn1); var tries: u32 = 0; while (tries < 1000) : (tries += 1) { const ready = try hal.sdmmc.cmd52Read(0, cccr_fn_ready); if (ready & fn1 != 0) { soc.rom.print("MARK SDIO_FN1 ready after %u polls\r\n", .{tries}); return; } soc.rom.ets_delay_us(1000); } return error.Timeout; } var hexbuf_a: [80]u8 = undefined; var hexbuf_b: [80]u8 = undefined; /// Bytes as lowercase hex into a caller-provided buffer, NUL-terminated for the ROM printer. fn hex(bytes: []const u8, len: usize, out: *[80]u8) [*:0]const u8 { const digits = "0123456789abcdef"; var i: usize = 0; while (i < len and i * 2 + 2 < out.len) : (i += 1) { out[i * 2] = digits[bytes[i] >> 4]; out[i * 2 + 1] = digits[bytes[i] & 0xF]; } out[i * 2] = 0; return @ptrCast(out); } fn park() noreturn { while (true) {} } export fn _start() linksection(".text.entry") callconv(.naked) noreturn { asm volatile ( \\ li t0, 1 << 13 \\ csrs mstatus, t0 \\ la sp, __stack_top \\ mv fp, sp \\ la t0, __bss_start \\ la t1, __bss_end \\ bgeu t0, t1, 2f \\1: \\ sw zero, 0(t0) \\ addi t0, t0, 4 \\ bltu t0, t1, 1b \\2: \\ j zig_main ); }