summaryrefslogtreecommitdiff
path: root/examples/halcheck.zig
blob: 94b3a2d5d6e766a52af21907b8f5917f325142af (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
//! Hardware check for the register layer and the first three HAL peripherals.
//!
//! Every line this prints is a claim about the silicon that the die itself answers. Nothing here is
//! a unit test of Zig code: the register numbers come from ESP-IDF's own macros, so the only thing
//! left to doubt is whether the *sequences* built on them do what the hardware needs.
//!
//! Run with:  zig build -Dapp=examples/halcheck.zig run -Dseconds=8

const std = @import("std");
const soc = @import("soc");
const hal = @import("hal");
const regs = @import("regs");
const mmio = @import("mmio");
const config = @import("config");

pub const panic = std.debug.FullPanic(struct {
    fn call(msg: []const u8, _: ?usize) noreturn {
        soc.rom.print("MARK HAL_PANIC %s\r\n", .{msg.ptr});
        while (true) {}
    }
}.call);

const led = config.led_pin;

export fn zig_main() noreturn {
    soc.rom.print("\r\nMARK HAL_START hw_ver=%u registers from ESP-IDF headers\r\n", .{
        @as(u32, regs.ZIG_P4_HW_VER),
    });

    // ---------------------------------------------------------------- 1. the register layer itself
    // A handful of addresses, printed so a human can check them against the technical reference
    // manual without trusting anything in this repo. GPIO_OUT is 0x500E0004 and IO_MUX's pad 0 is
    // 0x500E1004 on this part.
    soc.rom.print("MARK HAL_ADDR gpio_out=0x%08x iomux_pad0=0x%08x systimer_conf=0x%08x\r\n", .{
        @as(u32, @intCast(regs.GPIO_OUT_REG)),
        @as(u32, @intCast(regs.PERIPHS_IO_MUX_U_PAD_GPIO0)),
        @as(u32, @intCast(regs.SYSTIMER_CONF_REG)),
    });
    // The GPIO matrix constant that the hand-written predecessor got right and the first HAL draft
    // got wrong. 256 on the P4, 128 on the S3.
    soc.rom.print("MARK HAL_SIGMAP gpio_out_idx=%u expect=256\r\n", .{
        @as(u32, hal.gpio.matrix_gpio_signal),
    });

    // ---------------------------------------------------------------- 2. GPIO
    // Same observable behaviour as the hand-written GPIO this replaces: drive a pin, read it back
    // through the pad's own input buffer while it is driven.
    hal.gpio.configureOutput(led, .{ .readback = true });
    hal.gpio.setHigh(led);
    const drove_high = hal.gpio.getLevel(led);
    hal.gpio.setLow(led);
    const drove_low = hal.gpio.getLevel(led);
    soc.rom.print("MARK HAL_GPIO pin=%u high=%u low=%u oe=%u expect=1,0,1\r\n", .{
        @as(u32, led),
        @as(u32, drove_high),
        @as(u32, drove_low),
        @as(u32, @intFromBool(hal.gpio.isOutputEnabled(led))),
    });

    // The second bank. The split at pin 32 is the classic P4 GPIO bug - a write to `out` with a
    // shift of 40 lands on pin 8 - and it is only provable above 31.
    //
    // Pin choice matters here and cost a debugging round. GPIO54 is on JP1 but is this board's
    // ESP32-C6 reset line with an external pull-up: it drives correctly (out1 follows 1 -> 0) and
    // reads back stuck high, because the pull-up wins at the pad. GPIO33 (JP1 pin 21) is genuinely
    // free, and it tracks.
    const bank1_pin = 33;
    hal.gpio.configureOutput(bank1_pin, .{ .readback = true });
    hal.gpio.setHigh(bank1_pin);
    const hi_b1 = hal.gpio.getLevel(bank1_pin);
    hal.gpio.setLow(bank1_pin);
    const lo_b1 = hal.gpio.getLevel(bank1_pin);
    hal.gpio.outputDisable(bank1_pin);
    soc.rom.print("MARK HAL_GPIO_BANK1 pin=%u high=%u low=%u expect=1,0\r\n", .{
        @as(u32, bank1_pin), @as(u32, hi_b1), @as(u32, lo_b1),
    });

    // ---------------------------------------------------------------- 3. clock gates
    // TWAI0 is the one peripheral whose bus clock is gated OFF at power-on, which makes it the only
    // place where enabling a clock is observable rather than merely correct.
    //
    // Read what this proves narrowly. Both the write and the read-back go through this project's own
    // accessor, so agreement shows the two are consistent with each other - not that either touches
    // the right register. That is not a hypothetical caveat: an earlier version of clkrst.zig had
    // these four peripherals' gate bits in HP_SYS_CLKRST_PERI_CLK_CTRL21 instead of SOC_CLK_CTRL2,
    // and this test printed exactly the expected 0,1,0 while poking a bit of an unrelated register.
    // It would have passed with the chip unplugged. The real check is `zig build diff`, where the
    // reference reaches the hardware through ESP-IDF's LL instead.
    const twai_before = hal.clkrst.isClockEnabled(.twai0);
    hal.clkrst.setClockEnabled(.twai0, true);
    const twai_on = hal.clkrst.isClockEnabled(.twai0);
    hal.clkrst.setClockEnabled(.twai0, false);
    const twai_off = hal.clkrst.isClockEnabled(.twai0);
    soc.rom.print("MARK HAL_GATE twai0 boot=%u on=%u off=%u expect=0,1,0 (self-consistency only; see diff)\r\n", .{
        @as(u32, @intFromBool(twai_before)),
        @as(u32, @intFromBool(twai_on)),
        @as(u32, @intFromBool(twai_off)),
    });

    // UART0 is the console this text is coming out of, so its gate must read back enabled - and
    // this is a read, deliberately: writing it would cut the wire mid-sentence.
    soc.rom.print("MARK HAL_GATE uart0=%u expect=1 (console is alive, so it must be)\r\n", .{
        @as(u32, @intFromBool(hal.clkrst.isClockEnabled(.uart0))),
    });

    // ---------------------------------------------------------------- 4. systimer
    hal.systimer.init();
    const t0 = hal.systimer.read(.unit0) orelse {
        soc.rom.print("MARK HAL_SYSTIMER dead - no valid handshake\r\n", .{});
        hang();
    };
    const c0 = soc.cycles();
    soc.rom.ets_delay_us(50_000);
    const t1 = hal.systimer.read(.unit0) orelse unreachable;
    const c1 = soc.cycles();

    const ticks = t1 - t0;
    const cycles = c1 - c0;
    // The counter must advance, and by roughly 16 MHz * 50 ms = 800,000 ticks.
    soc.rom.print("MARK HAL_SYSTIMER ticks=%u expect~800000 advancing=%u\r\n", .{
        @as(u32, @truncate(ticks)),
        @as(u32, @intFromBool(t1 > t0)),
    });
    // Two independent clocks measuring the same interval: systimer is a fixed 16 MHz, so the CPU
    // frequency falls out of the ratio. This is the honest measurement of a number the README has
    // only ever estimated.
    const cpu_khz: u64 = if (ticks != 0) (cycles * (hal.systimer.hz / 1000)) / ticks else 0;
    soc.rom.print("MARK HAL_CPUFREQ %u kHz, from %u cycles per %u systimer ticks\r\n", .{
        @as(u32, @truncate(cpu_khz)),
        @as(u32, @truncate(cycles)),
        @as(u32, @truncate(ticks)),
    });

    // The 52-bit counter read as a pair of words. If the update/valid handshake were skipped, the
    // low word could wrap between the two loads and the value would jump backwards; sample it in a
    // tight loop and assert monotonicity, which is the only cheap way to catch that.
    var last: u64 = hal.systimer.read(.unit0) orelse unreachable;
    var backwards: u32 = 0;
    for (0..2000) |_| {
        const now = hal.systimer.read(.unit0) orelse unreachable;
        if (now < last) backwards += 1;
        last = now;
    }
    soc.rom.print("MARK HAL_SYSTIMER_MONOTONIC 2000 reads, backwards=%u expect=0\r\n", .{backwards});

    // ---------------------------------------------------------------- 5. the TIMG reset trap
    // Resetting a timer group re-arms flash-boot watchdog protection, and the board then reboots a
    // moment later with nothing on the console to explain it. resetPeripheral clears that bit as
    // part of the reset. The proof is negative and needs time to pass, so print before and after and
    // let the rest of this program be the delay.
    soc.rom.print("MARK HAL_TIMG_RESET resetting timg0 - a reboot after this line means the flashboot fixup is missing\r\n", .{});
    hal.clkrst.resetPeripheral(.timg0);
    hal.systimer.delayMicros(200_000);
    soc.rom.print("MARK HAL_TIMG_SURVIVED still running 200 ms after the timg0 reset\r\n", .{});

    soc.rom.print("MARK HAL_DONE\r\n", .{});

    // Blink from the HAL, using the systimer for the delay rather than the ROM's.
    var beat: u32 = 0;
    while (true) : (beat += 1) {
        hal.gpio.setHigh(led);
        hal.systimer.delayMicros(250_000);
        hal.gpio.setLow(led);
        hal.systimer.delayMicros(250_000);
        if (beat % 4 == 0) {
            soc.rom.print("MARK HAL_ALIVE beat=%u t=%u us\r\n", .{
                beat,
                @as(u32, @truncate(hal.systimer.micros(.unit0) orelse 0)),
            });
        }
    }
}

fn hang() 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
    );
}