diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/hal/clkrst.zig | 90 | ||||
| -rw-r--r-- | src/pardes/app.zig | 29 | ||||
| -rw-r--r-- | src/soc.zig | 5 |
3 files changed, 124 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; diff --git a/src/pardes/app.zig b/src/pardes/app.zig index 2f13865..5b1431f 100644 --- a/src/pardes/app.zig +++ b/src/pardes/app.zig @@ -185,6 +185,15 @@ export fn zig_main() noreturn { @as(u32, @intCast(heap.len / 1024)), }); + // The CPU clock, before anything is timed against it. The bootloader leaves 90 MHz and the + // CPLL is already at 360, so this is a divider change that disturbs neither UART0 (XTAL) nor + // the systimer (XTAL/2.5) nor the flash interface (SPLL). See hal/clkrst.zig:setCpuFreq. + if (config.cpu_mhz != 90) hal.clkrst.setCpuFreq(switch (config.cpu_mhz) { + 180 => .mhz180, + 360 => .mhz360, + else => .mhz90, + }); + const rwdt_was_armed = hal.rwdt.disable(); hal.systimer.init(); _ = rwdt_was_armed; @@ -208,6 +217,26 @@ export fn zig_main() noreturn { }); while (true) {} } + + // The CPU clock, measured rather than assumed. Every cycle count this firmware reports is + // divided by it somewhere, and `src/io/chip.zig` records it as "a measured ~90 MHz" that + // nothing here reconfigures - so it is worth printing rather than remembering. The systimer is + // XTAL/2.5 = 16 MHz and is NOT derived from the CPU clock (`hal/systimer.zig:31`, + // `clk_tree_defs.h:196-198`), which is exactly what makes it a valid reference for measuring it. + if (prof) { + const t_start = hal.systimer.micros(.unit0) orelse 0; + const c_start = soc.cycles(); + // 50 ms is long enough that the systimer's 16 MHz granularity and the loop's own overhead + // are both noise, and short enough to be invisible in a boot. + while ((hal.systimer.micros(.unit0) orelse 0) -% t_start < 50_000) {} + const elapsed_us = (hal.systimer.micros(.unit0) orelse 0) -% t_start; + const elapsed_cy = soc.cycles() - c_start; + soc.rom.print("MARK CPU_HZ cycles=%u us=%u khz=%u\r\n", .{ + @as(u32, @intCast(elapsed_cy)), + @as(u32, @intCast(elapsed_us)), + @as(u32, @intCast(if (elapsed_us > 0) elapsed_cy * 1000 / elapsed_us else 0)), + }); + } soc.rom.print("MARK PARDES_READY\r\n", .{}); var in: [256]u8 = undefined; diff --git a/src/soc.zig b/src/soc.zig index 350948a..1e94874 100644 --- a/src/soc.zig +++ b/src/soc.zig @@ -94,6 +94,11 @@ pub const rom = struct { pub extern fn ets_printf(fmt: [*:0]const u8, ...) c_int; pub extern fn ets_delay_us(us: u32) void; + /// Tell the ROM the CPU's new frequency, in MHz. `ets_delay_us` and everything else built on + /// `g_ticks_per_us` busy-waits by a cycle count derived from it, so a clock change without this + /// makes every ROM delay wrong by exactly the ratio. `esp32p4.rom.ld:32`, 0x4fc00044. + pub extern fn ets_update_cpu_frequency(mhz: u32) void; + /// Invalidate the caches. `map` selects which, from `rom/cache.h:228-236`: /// L1 ICache0 = 1, ICache1 = 2, L1 DCache = 0x10, L2 = 0x20; `cache_all` is all four. /// |
