summaryrefslogtreecommitdiff
path: root/src/hal
diff options
context:
space:
mode:
Diffstat (limited to 'src/hal')
-rw-r--r--src/hal/clkrst.zig90
1 files changed, 90 insertions, 0 deletions
diff --git a/src/hal/clkrst.zig b/src/hal/clkrst.zig
index 89a27ef..9902796 100644
--- a/src/hal/clkrst.zig
+++ b/src/hal/clkrst.zig
@@ -263,3 +263,93 @@ 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;