summaryrefslogtreecommitdiff
path: root/examples/halcheck.zig
diff options
context:
space:
mode:
Diffstat (limited to 'examples/halcheck.zig')
-rw-r--r--examples/halcheck.zig194
1 files changed, 194 insertions, 0 deletions
diff --git a/examples/halcheck.zig b/examples/halcheck.zig
new file mode 100644
index 0000000..94b3a2d
--- /dev/null
+++ b/examples/halcheck.zig
@@ -0,0 +1,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
+ );
+}