diff options
Diffstat (limited to 'src/hal/clkrst.zig')
| -rw-r--r-- | src/hal/clkrst.zig | 265 |
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); +} |
