summaryrefslogtreecommitdiff
path: root/examples/sdiocheck.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-25 12:40:53 -0300
committerGabriel Schneider <[email protected]>2026-08-25 12:46:51 -0300
commitf5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch)
tree2731a3ed4e51cae09e184e25778eded5fc37d1f5 /examples/sdiocheck.zig
downloadesp32p4-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.zig321
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
+ );
+}