summaryrefslogtreecommitdiff
path: root/src/hal/clkrst.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 /src/hal/clkrst.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 'src/hal/clkrst.zig')
-rw-r--r--src/hal/clkrst.zig265
1 files changed, 265 insertions, 0 deletions
diff --git a/src/hal/clkrst.zig b/src/hal/clkrst.zig
new file mode 100644
index 0000000..89a27ef
--- /dev/null
+++ b/src/hal/clkrst.zig
@@ -0,0 +1,265 @@
+//! Peripheral clock gates and resets: HP_SYS_CLKRST.
+//!
+//! Two things about this block are counter-intuitive on the ESP32-P4, and both were found by
+//! reading ESP-IDF rather than by assuming:
+//!
+//! **Peripheral clocks are already on.** `esp_system/port/soc/esp32p4/clk.c:200` says so in as many
+//! words - "All peripheral clocks are default enabled after chip is powered on" - and the reset
+//! values in `hp_sys_clkrst_reg.h` agree: REG_UART0_APB_CLK_EN, REG_TIMERGRP0_APB_CLK_EN,
+//! REG_SYSTIMER_APB_CLK_EN and REG_IOMUX_APB_CLK_EN all default to 1, with their RST_EN bits at 0.
+//! An image that boots from the stock second-stage bootloader never runs `esp_perip_clk_init`, so it
+//! inherits those defaults. So this file is not a prerequisite for touching a peripheral; it is what
+//! you need to *re*-initialise one, and to reach the few blocks that really are gated off (TWAI is
+//! the notable one: REG_TWAI0_APB_CLK_EN defaults to 0).
+//!
+//! **The hazard is atomicity, not gating.** Every gate and reset bit for the whole chip lives in a
+//! handful of shared registers, so `enable(.uart0)` is a read-modify-write of a word that also holds
+//! the gate for unrelated peripherals. ESP-IDF makes unguarded calls impossible to compile by
+//! referencing `__DECLARE_RCC_ATOMIC_ENV`, an identifier it never defines anywhere; the only legal
+//! callers are inside `PERIPH_RCC_ATOMIC()`, which takes a FreeRTOS spinlock. There is no FreeRTOS
+//! here and core 1 is held in reset at power-on (`hp_sys_clkrst_reg.h`: REG_RST_EN_CORE1_GLOBAL
+//! defaults to 1), so masking interrupts around the read-modify-write is sufficient and is what
+//! `atomically` does.
+
+const std = @import("std");
+const regs = @import("regs");
+const mmio = @import("mmio");
+
+const Reg = mmio.Reg;
+const Field = mmio.Field;
+
+// The four shared registers this file touches. Which field lives in which register is not derivable
+// from the macro names - `HP_SYS_CLKRST_REG_UART0_APB_CLK_EN_S` does not say `SOC_CLK_CTRL2` - so the
+// pairing is taken from ESP-IDF's own LL, cited per peripheral below.
+const soc_clk_ctrl1 = Reg.at(regs.HP_SYS_CLKRST_SOC_CLK_CTRL1_REG);
+const soc_clk_ctrl2 = Reg.at(regs.HP_SYS_CLKRST_SOC_CLK_CTRL2_REG);
+const soc_clk_ctrl3 = Reg.at(regs.HP_SYS_CLKRST_SOC_CLK_CTRL3_REG);
+/// SDMMC's reset bit is not in HP_SYS_CLKRST at all. `sdmmc_ll_reset_register`
+/// (`sdmmc_ll.h:158-163`) writes `LP_AON_CLKRST.hp_sdmmc_emac_rst_ctrl.rst_en_sdmmc`, a register
+/// of the *low-power* always-on clock-and-reset block, which it shares with the Ethernet MAC. So
+/// the `Gates.reset` field is a register as well as a bit, and this is the row that proves it has
+/// to be.
+const lp_hp_sdmmc_emac_rst_ctrl = Reg.at(regs.LP_CLKRST_HP_SDMMC_EMAC_RST_CTRL_REG);
+const hp_rst_en1 = Reg.at(regs.HP_SYS_CLKRST_HP_RST_EN1_REG);
+
+/// Interrupts masked for the duration of a read-modify-write on a shared register:
+///
+/// const guard = clkrst.maskInterrupts();
+/// defer guard.release();
+///
+/// mstatus.MIE is bit 3. `csrrc` clears it and returns the previous mstatus in one instruction, and
+/// `release` restores only what was actually there - so this composes: using it inside code that
+/// already had interrupts off does not turn them on at the end.
+pub const Guard = struct {
+ prev_mie: bool,
+
+ pub inline fn release(self: Guard) void {
+ if (self.prev_mie) {
+ asm volatile ("csrs mstatus, %[mask]"
+ :
+ : [mask] "r" (@as(u32, 1 << 3)),
+ );
+ }
+ }
+};
+
+pub inline fn maskInterrupts() Guard {
+ const prev = asm volatile ("csrrc %[out], mstatus, %[mask]"
+ : [out] "=r" (-> u32),
+ : [mask] "r" (@as(u32, 1 << 3)),
+ );
+ return .{ .prev_mie = prev & (1 << 3) != 0 };
+}
+
+/// A peripheral's clock gates and reset bit.
+///
+/// `sys_clk` is present only where the peripheral has a second gate on the SYS clock as well as the
+/// APB one; UART has both (uart_ll.h:252-253 reads `soc_clk_ctrl2.reg_uart0_apb_clk_en` and
+/// `soc_clk_ctrl1.reg_uart0_sys_clk_en`), most blocks have only APB.
+const Gates = struct {
+ apb_clk: ?struct { reg: Reg, field: Field } = null,
+ sys_clk: ?struct { reg: Reg, field: Field } = null,
+ reset: struct { reg: Reg, field: Field },
+ /// TIMG only: resetting the block re-arms flash-boot protection, which reboots the board a
+ /// moment later with no diagnostic. `timg_ll.h:53-72` documents it and clears the bit as part of
+ /// the reset; anything that resets TIMG must do the same.
+ clears_flashboot: bool = false,
+};
+
+pub const Peripheral = enum {
+ uart0,
+ uart1,
+ uart2,
+ uart3,
+ uart4,
+ timg0,
+ timg1,
+ systimer,
+ twai0,
+ ledc,
+ i2c0,
+ i2c1,
+ sdmmc,
+
+ fn gates(comptime self: Peripheral) Gates {
+ return switch (self) {
+ // uart_ll.h:251-253 for UART0, and the same three fields per instance after it.
+ .uart0 => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_UART0_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_UART0_APB_CLK_EN_V) },
+ .sys_clk = .{ .reg = soc_clk_ctrl1, .field = Field.of(regs.HP_SYS_CLKRST_REG_UART0_SYS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_UART0_SYS_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_UART0_APB_S, regs.HP_SYS_CLKRST_REG_RST_EN_UART0_APB_V) },
+ },
+ .uart1 => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_UART1_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_UART1_APB_CLK_EN_V) },
+ .sys_clk = .{ .reg = soc_clk_ctrl1, .field = Field.of(regs.HP_SYS_CLKRST_REG_UART1_SYS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_UART1_SYS_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_UART1_APB_S, regs.HP_SYS_CLKRST_REG_RST_EN_UART1_APB_V) },
+ },
+ .uart2 => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_UART2_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_UART2_APB_CLK_EN_V) },
+ .sys_clk = .{ .reg = soc_clk_ctrl1, .field = Field.of(regs.HP_SYS_CLKRST_REG_UART2_SYS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_UART2_SYS_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_UART2_APB_S, regs.HP_SYS_CLKRST_REG_RST_EN_UART2_APB_V) },
+ },
+ .uart3 => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_UART3_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_UART3_APB_CLK_EN_V) },
+ .sys_clk = .{ .reg = soc_clk_ctrl1, .field = Field.of(regs.HP_SYS_CLKRST_REG_UART3_SYS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_UART3_SYS_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_UART3_APB_S, regs.HP_SYS_CLKRST_REG_RST_EN_UART3_APB_V) },
+ },
+ .uart4 => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_UART4_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_UART4_APB_CLK_EN_V) },
+ .sys_clk = .{ .reg = soc_clk_ctrl1, .field = Field.of(regs.HP_SYS_CLKRST_REG_UART4_SYS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_UART4_SYS_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_UART4_APB_S, regs.HP_SYS_CLKRST_REG_RST_EN_UART4_APB_V) },
+ },
+ // timg_ll.h:35-42 for the gate, :60-72 for the reset. The timer groups' APB gate is in
+ // SOC_CLK_CTRL2 - the same word as the UARTs' - not in PERI_CLK_CTRL21. An earlier
+ // version of this table had these four entries in PERI_CLK_CTRL21 and so wrote bits
+ // 21-24 of an unrelated register; hp_sys_clkrst_reg.h:605 defines SOC_CLK_CTRL2_REG and
+ // :753/:763/:770/:777 put TIMERGRP0 at bit 21, TIMERGRP1 at 22, SYSTIMER at 23 and
+ // TWAI0 at 24 inside it. PERI_CLK_CTRL20/21 do hold timer-group fields - the per-timer
+ // clock source and gate, see hal/timg.zig - which is what made the mix-up plausible.
+ //
+ // It survived a hardware check because `isClockEnabled` read back the same wrong bit
+ // `setClockEnabled` had just written: self-consistent, and independent of the chip.
+ .timg0 => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP0_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TIMERGRP0_APB_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_TIMERGRP0_S, regs.HP_SYS_CLKRST_REG_RST_EN_TIMERGRP0_V) },
+ .clears_flashboot = true,
+ },
+ .timg1 => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP1_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TIMERGRP1_APB_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_TIMERGRP1_S, regs.HP_SYS_CLKRST_REG_RST_EN_TIMERGRP1_V) },
+ .clears_flashboot = true,
+ },
+ // systimer_ll.h:71-72.
+ .systimer => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_SYSTIMER_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SYSTIMER_APB_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_STIMER_S, regs.HP_SYS_CLKRST_REG_RST_EN_STIMER_V) },
+ },
+ // The one block whose clock is gated OFF at power-on, which makes it the only peripheral
+ // where `enable` is observably necessary rather than merely correct.
+ .twai0 => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_TWAI0_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TWAI0_APB_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_TWAI0_S, regs.HP_SYS_CLKRST_REG_RST_EN_TWAI0_V) },
+ },
+ // ledc_ll.h:135 for the gate (`HP_SYS_CLKRST.soc_clk_ctrl3.reg_ledc_apb_clk_en`) and
+ // :150 for the reset (`hp_rst_en1.reg_rst_en_ledc`). LEDC's APB gate is the *first* bit
+ // of SOC_CLK_CTRL3, a third register this table did not previously need, and it is one
+ // of the few whose reset value is 0 (hp_sys_clkrst_reg.h:835): LEDC's registers are
+ // gated off at power-on, so `setClockEnabled(.ledc, true)` is a prerequisite and not a
+ // formality. LEDC's *function* clock and its source mux live in PERI_CLK_CTRL22
+ // (ledc_ll.h:179, :241) and belong to the peripheral, not to this table - see
+ // hal/ledc.zig.
+ .ledc => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl3, .field = Field.of(regs.HP_SYS_CLKRST_REG_LEDC_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_LEDC_APB_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_LEDC_S, regs.HP_SYS_CLKRST_REG_RST_EN_LEDC_V) },
+ },
+ // i2c_ll.h:149-156 for the gates (`HP_SYS_CLKRST.soc_clk_ctrl2.reg_i2c0_apb_clk_en`,
+ // and `reg_i2c1_apb_clk_en` for port 1) and :167-176 for the resets
+ // (`hp_rst_en1.reg_rst_en_i2c0` / `_i2c1`). Both APB gates default to 1
+ // (hp_sys_clkrst_reg.h:694-703), so the registers are reachable from boot; what I2C
+ // does *not* get from this table is its controller clock, whose enable, source mux and
+ // divider are I2C-specific fields of PERI_CLK_CTRL10/11 and live in hal/i2c.zig. That
+ // one defaults to 0, so an I2C port brought up through this table alone has readable
+ // registers and a state machine that never moves.
+ .i2c0 => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_I2C0_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_I2C0_APB_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_I2C0_S, regs.HP_SYS_CLKRST_REG_RST_EN_I2C0_V) },
+ },
+ .i2c1 => .{
+ .apb_clk = .{ .reg = soc_clk_ctrl2, .field = Field.of(regs.HP_SYS_CLKRST_REG_I2C1_APB_CLK_EN_S, regs.HP_SYS_CLKRST_REG_I2C1_APB_CLK_EN_V) },
+ .reset = .{ .reg = hp_rst_en1, .field = Field.of(regs.HP_SYS_CLKRST_REG_RST_EN_I2C1_S, regs.HP_SYS_CLKRST_REG_RST_EN_I2C1_V) },
+ },
+ // The one row in this table whose two halves live in two different peripherals, and
+ // the one whose clock really is gated off at power-on alongside LEDC's.
+ //
+ // `sdmmc_ll.h:140-144` is the gate: `HP_SYS_CLKRST.soc_clk_ctrl1.reg_sdmmc_sys_clk_en`,
+ // a *SYS* clock and not an APB one - SDMMC has no APB gate at all, which is why the
+ // `apb_clk` field is absent here rather than merely unused. It defaults to 0
+ // (hp_sys_clkrst_reg.h:475-481, "default: 0"), so `setClockEnabled(.sdmmc, true)` is a
+ // prerequisite for the register block reading anything but stale values.
+ //
+ // `sdmmc_ll.h:158-163` is the reset, and it is in LP_AON_CLKRST:
+ // `hp_sdmmc_emac_rst_ctrl.rst_en_sdmmc`, bit 28 (lp_clkrst_reg.h:993-999). Looking for
+ // an `HP_SYS_CLKRST_REG_RST_EN_SDMMC` finds nothing, which is exactly the shape of the
+ // mistake the timer-group rows above record: a plausible name in the wrong register.
+ //
+ // The host clock generator - source mux, divider, sampling phase - is *not* here. It
+ // is SDMMC-specific and lives in PERI_CLK_CTRL01/02, in hal/sdmmc.zig, the same
+ // division this table makes for I2C and LEDC.
+ .sdmmc => .{
+ .sys_clk = .{ .reg = soc_clk_ctrl1, .field = Field.of(regs.HP_SYS_CLKRST_REG_SDMMC_SYS_CLK_EN_S, regs.HP_SYS_CLKRST_REG_SDMMC_SYS_CLK_EN_V) },
+ .reset = .{ .reg = lp_hp_sdmmc_emac_rst_ctrl, .field = Field.of(regs.LP_CLKRST_RST_EN_SDMMC_S, regs.LP_CLKRST_RST_EN_SDMMC_V) },
+ },
+ };
+ }
+};
+
+/// Turn a peripheral's bus clocks on or off.
+pub fn setClockEnabled(comptime p: Peripheral, on: bool) void {
+ const g = comptime p.gates();
+ const v: u32 = @intFromBool(on);
+ const guard = maskInterrupts();
+ defer guard.release();
+ if (g.sys_clk) |s| s.reg.modify(.{s.field.is(v)});
+ if (g.apb_clk) |a| a.reg.modify(.{a.field.is(v)});
+}
+
+/// Whether the peripheral's bus clock is on.
+///
+/// APB gate if it has one, SYS gate otherwise: SDMMC has only the latter (`sdmmc_ll.h:140-144`),
+/// and answering `true` unconditionally for it would have made the oracle's clock check - the one
+/// that exists because a gated block reads stale rather than zero - pass on a gated block.
+pub fn isClockEnabled(comptime p: Peripheral) bool {
+ const g = comptime p.gates();
+ if (g.apb_clk) |a| return a.reg.get(a.field) == 1;
+ if (g.sys_clk) |s| return s.reg.get(s.field) == 1;
+ return true;
+}
+
+/// Pulse a peripheral's reset: assert, deassert.
+///
+/// For the timer groups this also clears flash-boot watchdog protection, which the reset re-arms.
+/// Leaving that out reboots the board a moment later with nothing on the console to explain it.
+pub fn resetPeripheral(comptime p: Peripheral) void {
+ const g = comptime p.gates();
+ {
+ const guard = maskInterrupts();
+ defer guard.release();
+ g.reset.reg.modify(.{g.reset.field.is(1)});
+ g.reset.reg.modify(.{g.reset.field.is(0)});
+ }
+ if (comptime g.clears_flashboot) {
+ const wdtconfig0 = Reg.atAddress(switch (p) {
+ .timg0 => regs.TIMG_WDTCONFIG0_REG(0),
+ .timg1 => regs.TIMG_WDTCONFIG0_REG(1),
+ else => unreachable,
+ });
+ wdtconfig0.modify(.{Field.of(regs.TIMG_WDT_FLASHBOOT_MOD_EN_S, regs.TIMG_WDT_FLASHBOOT_MOD_EN_V).is(0)});
+ }
+}
+
+/// Reset a peripheral and make sure its clocks are on, in that order: a peripheral configured
+/// before its reset is released loses the configuration.
+pub fn init(comptime p: Peripheral) void {
+ setClockEnabled(p, true);
+ resetPeripheral(p);
+}