//! 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); } // --------------------------------------------------------------------------- the CPU's own clock /// Raise the HP CPU clock from the 90 MHz the bootloader leaves to `mhz`. /// /// WHY THIS IS CHEAP. The CPLL is ALREADY at 360 MHz: 90 is exactly 360/4, and the stock /// second-stage bootloader gets there by setting `CONFIG_BOOTLOADER_CPU_CLK_FREQ_MHZ = 90` /// (`bootloader_support/src/bootloader_clock_init.c:27-37`). So this is a divider change and /// nothing else - no PLL to enable, no lock to wait for, and on the P4 no voltage step exists to /// order it against (`esp_hw_support/port/esp32p4/rtc_clk_init.c:58-80` sets HP_ACTIVE DBIAS once /// from efuse and never per-frequency). /// /// WHAT IT DOES NOT DISTURB, which is the reason it is safe to do from a running console: /// * UART0's baud clock is selected by `PERI_CLK_CTRL110[25:24]` from XTAL, RC_FAST or PLL_F80M /// (`hal/uart.zig:116-139`) - never the CPU clock. The console keeps its rate. /// * The systimer is XTAL/2.5 = 16 MHz (`hal/systimer.zig:31`), so every timeout built on /// `nowMs` keeps meaning what it meant. /// * The flash interface runs from SPLL 480 MHz (`spimem_flash_ll.h:676-684`), so code executing /// from flash-mapped memory is unaffected and this need not run from RAM. /// * The `cycle` CSR counts real CPU cycles, so it simply counts faster. Nothing on the board /// caches a cycles-per-microsecond figure; the HOST divisor in `experiments/` must move. /// /// The divider set and the ORDER are ESP-IDF's, from `rtc_clk_cpu_freq_to_cpll_mhz` /// (`esp_hw_support/port/esp32p4/rtc_clk.c`). Only three CPU frequencies are legal on pre-v3 /// silicon and each pins MEM/SYS/APB with it, because MEM must stay <= 200 MHz and APB <= 100: /// /// CPU 360 = CPLL/1, MEM = CPU/2 = 180, SYS = MEM/1 = 180, APB = SYS/2 = 90 /// CPU 180 = CPLL/2, MEM = CPU/1 = 180, SYS = MEM/1 = 180, APB = SYS/2 = 90 /// CPU 90 = CPLL/4, MEM = CPU/1 = 90, SYS = MEM/1 = 90, APB = SYS/1 = 90 /// /// APB lands at 90 MHz in all three, which is why peripherals do not care. Upscaling walks /// APB -> SYS -> MEM -> CPU with a bus update after each: IDF's comment is explicit that the other /// order passes through an intermediate state where APB or MEM violates its timing, and anything /// touching those clocks during it may fault. pub const CpuFreq = enum(u16) { mhz90 = 90, mhz180 = 180, mhz360 = 360 }; pub fn setCpuFreq(target: CpuFreq) void { const root0 = Reg.at(regs.HP_SYS_CLKRST_ROOT_CLK_CTRL0_REG); const root1 = Reg.at(regs.HP_SYS_CLKRST_ROOT_CLK_CTRL1_REG); const root2 = Reg.at(regs.HP_SYS_CLKRST_ROOT_CLK_CTRL2_REG); const cpu_div = Field.of(regs.HP_SYS_CLKRST_REG_CPU_CLK_DIV_NUM_S, regs.HP_SYS_CLKRST_REG_CPU_CLK_DIV_NUM_V); const cpu_num = Field.of(regs.HP_SYS_CLKRST_REG_CPU_CLK_DIV_NUMERATOR_S, regs.HP_SYS_CLKRST_REG_CPU_CLK_DIV_NUMERATOR_V); const cpu_den = Field.of(regs.HP_SYS_CLKRST_REG_CPU_CLK_DIV_DENOMINATOR_S, regs.HP_SYS_CLKRST_REG_CPU_CLK_DIV_DENOMINATOR_V); const mem_div = Field.of(regs.HP_SYS_CLKRST_REG_MEM_CLK_DIV_NUM_S, regs.HP_SYS_CLKRST_REG_MEM_CLK_DIV_NUM_V); const sys_div = Field.of(regs.HP_SYS_CLKRST_REG_SYS_CLK_DIV_NUM_S, regs.HP_SYS_CLKRST_REG_SYS_CLK_DIV_NUM_V); const apb_div = Field.of(regs.HP_SYS_CLKRST_REG_APB_CLK_DIV_NUM_S, regs.HP_SYS_CLKRST_REG_APB_CLK_DIV_NUM_V); const update = Field.of(regs.HP_SYS_CLKRST_REG_SOC_CLK_DIV_UPDATE_S, regs.HP_SYS_CLKRST_REG_SOC_CLK_DIV_UPDATE_V); // Every divider register holds `divider - 1`. const plan: struct { cpu: u32, mem: u32, sys: u32, apb: u32 } = switch (target) { .mhz360 => .{ .cpu = 1, .mem = 2, .sys = 1, .apb = 2 }, .mhz180 => .{ .cpu = 2, .mem = 1, .sys = 1, .apb = 2 }, .mhz90 => .{ .cpu = 4, .mem = 1, .sys = 1, .apb = 1 }, }; // The update bit is self-clearing and gates the whole divider set at once. Bounded, because an // unbounded spin on a board with no debugger is indistinguishable from a crash. const commit = struct { fn go(r: Reg, f: Field) void { r.modify(.{f.is(1)}); _ = r.waitFor(f, 0, 100_000); } }.go; // Upscaling only: this firmware boots at 90 and never lowers. Doing it in the downscale order // would leave APB above its 100 MHz limit while CPU was already fast. root2.modify(.{apb_div.is(plan.apb - 1)}); commit(root0, update); root1.modify(.{sys_div.is(plan.sys - 1)}); commit(root0, update); root1.modify(.{mem_div.is(plan.mem - 1)}); commit(root0, update); root0.modify(.{ cpu_div.is(plan.cpu - 1), cpu_num.is(0), cpu_den.is(0) }); commit(root0, update); // The source mux is NOT covered by the update bit and must move last; it is already CPLL here, // so this is a no-op that documents the requirement rather than a step that changes anything. // // Then tell the mask ROM, because `ets_delay_us` and anything else built on `g_ticks_per_us` // would otherwise delay by the wrong factor. `ets_update_cpu_frequency` is the recalibrator // (`esp32p4.rom.ld:32`, 0x4fc00044). ets_update_cpu_frequency(@intFromEnum(target)); } /// Declared here rather than reached through `soc.rom`, because `soc` imports `hal` and the edge /// cannot run both ways. It is a bare linker symbol either way - `build.zig` defines the address /// once for the whole image - so a second declaration of it costs nothing and keeps the frequency /// change and its recalibration in one function, where forgetting the second is impossible. extern fn ets_update_cpu_frequency(mhz: u32) void;