diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 12:40:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 12:46:51 -0300 |
| commit | f5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch) | |
| tree | 2731a3ed4e51cae09e184e25778eded5fc37d1f5 /examples/sdiocheck.zig | |
| download | esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip | |
zig-p4: pure-Zig ESP32-P4 toolchain
build.zig generates the linker script and drives Zig's own LLD; tools/image.zig
turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask
ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker.
src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers;
src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip;
src/oracle/ diffs this HAL against ESP-IDF's on the die.
Diffstat (limited to 'examples/sdiocheck.zig')
| -rw-r--r-- | examples/sdiocheck.zig | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/examples/sdiocheck.zig b/examples/sdiocheck.zig new file mode 100644 index 0000000..d9fde11 --- /dev/null +++ b/examples/sdiocheck.zig @@ -0,0 +1,321 @@ +//! 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 + ); +} |
