summaryrefslogtreecommitdiff
path: root/examples/sdiocheck.zig
blob: d9fde1160bbaece81dc892c46e2e854a4ffe0ee1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
//! 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
    );
}