//! 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 ); }