summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig11
-rw-r--r--experiments/length-CPU360-final.csv46
-rw-r--r--experiments/length-CPU360-fine.csv46
-rw-r--r--experiments/length-CPU360.csv36
-rw-r--r--src/hal/clkrst.zig90
-rw-r--r--src/pardes/app.zig29
-rw-r--r--src/soc.zig5
-rw-r--r--tools/rtt.zig11
8 files changed, 271 insertions, 3 deletions
diff --git a/build.zig b/build.zig
index 3053d13..37156ca 100644
--- a/build.zig
+++ b/build.zig
@@ -108,6 +108,14 @@ pub fn build(b: *std.Build) void {
// cycle counts. Off by default because it puts a line on the wire per frame, which is the very
// resource being measured - it answers "where did the 34 ms go", not "how fast is it".
options.addOption(bool, "prof", b.option(bool, "prof", "print per-phase cycle counts (pardes)") orelse false);
+ // The CPU clock, in MHz. The bootloader leaves 90; the CPLL is already at 360, so 180 and 360
+ // are a divider change away and nothing else (see hal/clkrst.zig:setCpuFreq). Opt-in rather
+ // than default because it is the one setting here that changes how every other timing in the
+ // image behaves, and because 90 is what every measurement in experiments/ was taken against.
+ const cpu_mhz = b.option(u16, "cpu-mhz", "HP CPU clock: 90 (bootloader default), 180 or 360") orelse 90;
+ if (cpu_mhz != 90 and cpu_mhz != 180 and cpu_mhz != 360)
+ std.debug.panic("-Dcpu-mhz must be 90, 180 or 360; the P4's CPLL divides 360 by 4, 2 or 1", .{});
+ options.addOption(u16, "cpu_mhz", cpu_mhz);
options.addOption([]const u8, "wifi_ssid", wifi_ssid);
options.addOption([]const u8, "wifi_psk", if (psk_file) |path| blk: {
const raw = std.Io.Dir.cwd().readFileAlloc(b.graph.io, path, b.allocator, .limited(256)) catch
@@ -1216,6 +1224,9 @@ fn linkerScript(b: *std.Build, stack_size: u32, peripherals_ld: ?[]const u8) []c
\\/* ESP32-P4 mask ROM entry points, the only "library" this image links against */
\\ets_printf = 0x4fc00024;
\\ets_delay_us = 0x4fc0003c;
+ \\/* Recalibrate the ROM's cycles-per-microsecond after a CPU clock change; without it every
+ \\ ets_delay_us is wrong by exactly the frequency ratio (esp32p4.rom.ld:32). */
+ \\ets_update_cpu_frequency = 0x4fc00044;
\\/* Invalidate the caches. The bootloader leaves lines that do not match the mapping it
\\ finally installs, so a large image reads its own .rodata and gets its own .text back. */
\\Cache_Invalidate_All = 0x4fc00404;
diff --git a/experiments/length-CPU360-final.csv b/experiments/length-CPU360-final.csv
new file mode 100644
index 0000000..b7505d6
--- /dev/null
+++ b/experiments/length-CPU360-final.csv
@@ -0,0 +1,46 @@
+label,experiment,cols,rows,length,op,rep,rtt_us,settle_us,bytes
+CPU360-final,length,0,0,0,insert,0,4654,16016,141
+CPU360-final,length,0,0,0,insert,1,4378,10489,80
+CPU360-final,length,0,0,0,insert,2,4332,10601,81
+CPU360-final,length,0,0,0,insert,3,4435,10512,81
+CPU360-final,length,0,0,0,insert,4,4366,10438,81
+CPU360-final,length,0,0,0,insert,5,4362,10559,81
+CPU360-final,length,0,0,0,insert,6,4320,10559,81
+CPU360-final,length,0,0,0,insert,7,4327,10506,81
+CPU360-final,length,0,0,0,insert,8,4329,10533,81
+CPU360-final,length,0,0,20,insert,0,4371,10658,81
+CPU360-final,length,0,0,20,insert,1,4435,10483,81
+CPU360-final,length,0,0,20,insert,2,4431,10521,81
+CPU360-final,length,0,0,20,insert,3,4407,17349,158
+CPU360-final,length,0,0,20,insert,4,4370,10529,80
+CPU360-final,length,0,0,20,insert,5,4407,10547,81
+CPU360-final,length,0,0,20,insert,6,4400,10558,81
+CPU360-final,length,0,0,20,insert,7,4425,10539,81
+CPU360-final,length,0,0,20,insert,8,4480,10644,81
+CPU360-final,length,0,0,40,insert,0,4452,10652,81
+CPU360-final,length,0,0,40,insert,1,4506,10507,81
+CPU360-final,length,0,0,40,insert,2,4445,10609,81
+CPU360-final,length,0,0,40,insert,3,4509,10769,81
+CPU360-final,length,0,0,40,insert,4,4455,10535,81
+CPU360-final,length,0,0,40,insert,5,4492,10556,81
+CPU360-final,length,0,0,40,insert,6,4479,17386,158
+CPU360-final,length,0,0,40,insert,7,4447,10470,80
+CPU360-final,length,0,0,40,insert,8,4458,10702,81
+CPU360-final,length,0,0,80,insert,0,4534,10584,81
+CPU360-final,length,0,0,80,insert,1,4505,10713,81
+CPU360-final,length,0,0,80,insert,2,4506,10615,81
+CPU360-final,length,0,0,80,insert,3,4507,10669,81
+CPU360-final,length,0,0,80,insert,4,4593,10818,81
+CPU360-final,length,0,0,80,insert,5,4578,10757,81
+CPU360-final,length,0,0,80,insert,6,4530,10699,81
+CPU360-final,length,0,0,80,insert,7,4555,10660,81
+CPU360-final,length,0,0,80,insert,8,4512,10691,81
+CPU360-final,length,0,0,160,insert,0,4716,10842,81
+CPU360-final,length,0,0,160,insert,1,4662,10811,81
+CPU360-final,length,0,0,160,insert,2,4665,10787,81
+CPU360-final,length,0,0,160,insert,3,4665,10771,81
+CPU360-final,length,0,0,160,insert,4,4691,10800,81
+CPU360-final,length,0,0,160,insert,5,4643,10841,81
+CPU360-final,length,0,0,160,insert,6,4653,10806,81
+CPU360-final,length,0,0,160,insert,7,4670,10899,81
+CPU360-final,length,0,0,160,insert,8,4690,10890,81
diff --git a/experiments/length-CPU360-fine.csv b/experiments/length-CPU360-fine.csv
new file mode 100644
index 0000000..1cabf17
--- /dev/null
+++ b/experiments/length-CPU360-fine.csv
@@ -0,0 +1,46 @@
+label,experiment,cols,rows,length,op,rep,rtt_us,settle_us,bytes
+CPU360-fine,length,0,0,0,insert,0,4684,15988,141
+CPU360-fine,length,0,0,0,insert,1,4438,10511,80
+CPU360-fine,length,0,0,0,insert,2,4375,10539,81
+CPU360-fine,length,0,0,0,insert,3,4441,10641,81
+CPU360-fine,length,0,0,0,insert,4,4417,10594,81
+CPU360-fine,length,0,0,0,insert,5,4396,10583,81
+CPU360-fine,length,0,0,0,insert,6,4340,10492,81
+CPU360-fine,length,0,0,0,insert,7,4367,10471,81
+CPU360-fine,length,0,0,0,insert,8,4391,10472,81
+CPU360-fine,length,0,0,20,insert,0,4401,10650,81
+CPU360-fine,length,0,0,20,insert,1,4419,10660,81
+CPU360-fine,length,0,0,20,insert,2,4427,10583,81
+CPU360-fine,length,0,0,20,insert,3,4496,17324,158
+CPU360-fine,length,0,0,20,insert,4,5204,10552,80
+CPU360-fine,length,0,0,20,insert,5,4391,12938,81
+CPU360-fine,length,0,0,20,insert,6,4401,10649,81
+CPU360-fine,length,0,0,20,insert,7,4442,10688,81
+CPU360-fine,length,0,0,20,insert,8,4387,10635,81
+CPU360-fine,length,0,0,40,insert,0,4405,10608,81
+CPU360-fine,length,0,0,40,insert,1,4404,10576,81
+CPU360-fine,length,0,0,40,insert,2,4443,10690,81
+CPU360-fine,length,0,0,40,insert,3,4417,10706,81
+CPU360-fine,length,0,0,40,insert,4,4416,10656,81
+CPU360-fine,length,0,0,40,insert,5,4541,10646,81
+CPU360-fine,length,0,0,40,insert,6,4497,17324,158
+CPU360-fine,length,0,0,40,insert,7,4471,10567,80
+CPU360-fine,length,0,0,40,insert,8,4493,10735,81
+CPU360-fine,length,0,0,80,insert,0,4578,10838,81
+CPU360-fine,length,0,0,80,insert,1,4605,10844,81
+CPU360-fine,length,0,0,80,insert,2,4524,10767,81
+CPU360-fine,length,0,0,80,insert,3,4585,10665,81
+CPU360-fine,length,0,0,80,insert,4,4666,10826,81
+CPU360-fine,length,0,0,80,insert,5,4608,10706,81
+CPU360-fine,length,0,0,80,insert,6,4564,10756,81
+CPU360-fine,length,0,0,80,insert,7,4586,10794,81
+CPU360-fine,length,0,0,80,insert,8,4530,10629,81
+CPU360-fine,length,0,0,160,insert,0,4618,10778,81
+CPU360-fine,length,0,0,160,insert,1,4708,10889,81
+CPU360-fine,length,0,0,160,insert,2,4715,10946,81
+CPU360-fine,length,0,0,160,insert,3,4712,10854,81
+CPU360-fine,length,0,0,160,insert,4,4653,10940,81
+CPU360-fine,length,0,0,160,insert,5,4655,10975,81
+CPU360-fine,length,0,0,160,insert,6,4639,10993,81
+CPU360-fine,length,0,0,160,insert,7,4684,10835,81
+CPU360-fine,length,0,0,160,insert,8,4691,10915,81
diff --git a/experiments/length-CPU360.csv b/experiments/length-CPU360.csv
new file mode 100644
index 0000000..62f2f95
--- /dev/null
+++ b/experiments/length-CPU360.csv
@@ -0,0 +1,36 @@
+label,experiment,cols,rows,length,op,rep,rtt_us,settle_us,bytes
+CPU360,length,0,0,0,insert,0,4640,16152,141
+CPU360,length,0,0,0,insert,1,4401,10465,80
+CPU360,length,0,0,0,insert,2,4458,10618,81
+CPU360,length,0,0,0,insert,3,4349,10504,81
+CPU360,length,0,0,0,insert,4,4445,10554,81
+CPU360,length,0,0,0,insert,5,4369,10652,81
+CPU360,length,0,0,0,insert,6,4397,10498,81
+CPU360,length,0,0,20,insert,0,4371,10644,81
+CPU360,length,0,0,20,insert,1,4393,10545,81
+CPU360,length,0,0,20,insert,2,4390,10548,81
+CPU360,length,0,0,20,insert,3,4378,10585,81
+CPU360,length,0,0,20,insert,4,4463,10590,81
+CPU360,length,0,0,20,insert,5,4465,17451,158
+CPU360,length,0,0,20,insert,6,4418,10533,80
+CPU360,length,0,0,40,insert,0,4506,10670,81
+CPU360,length,0,0,40,insert,1,4496,10680,81
+CPU360,length,0,0,40,insert,2,4452,10645,81
+CPU360,length,0,0,40,insert,3,4521,10660,81
+CPU360,length,0,0,40,insert,4,4421,10655,81
+CPU360,length,0,0,40,insert,5,4561,10801,81
+CPU360,length,0,0,40,insert,6,4471,10641,81
+CPU360,length,0,0,80,insert,0,4532,10709,81
+CPU360,length,0,0,80,insert,1,4566,10717,81
+CPU360,length,0,0,80,insert,2,4528,10678,81
+CPU360,length,0,0,80,insert,3,4553,10733,81
+CPU360,length,0,0,80,insert,4,4507,10805,81
+CPU360,length,0,0,80,insert,5,4547,10773,81
+CPU360,length,0,0,80,insert,6,4536,10637,81
+CPU360,length,0,0,160,insert,0,4720,10867,81
+CPU360,length,0,0,160,insert,1,4677,10832,81
+CPU360,length,0,0,160,insert,2,4673,10873,81
+CPU360,length,0,0,160,insert,3,4646,10752,81
+CPU360,length,0,0,160,insert,4,4729,17477,158
+CPU360,length,0,0,160,insert,5,4640,10674,80
+CPU360,length,0,0,160,insert,6,4679,10795,81
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.
///
diff --git a/tools/rtt.zig b/tools/rtt.zig
index 6ab0d87..d971c74 100644
--- a/tools/rtt.zig
+++ b/tools/rtt.zig
@@ -62,9 +62,14 @@ pub fn roundTrip(
if (now - t0 > timeout_us) return null;
} else if (now - last > quiet_us) break;
- // Poll in millisecond units because that is what poll(2) takes; the TIMING above is
- // microseconds and independent of this granularity.
- const n = try port.readTimeout(&buf, 1);
+ // NON-BLOCKING, and that matters. `poll(2)` takes a timeout in MILLISECONDS, so waiting
+ // even 1 ms quantises this measurement into 1 ms buckets: a response that arrived just after
+ // a poll returned empty is reported up to a millisecond late. Against a round trip of a few
+ // milliseconds that is not a rounding error, it is a large fraction of the answer - measured
+ // as ~0.75 ms of unexplained gap between this figure and the sum of the board's own stage
+ // timings. A spin costs a busy host CPU for a few milliseconds per sample, which is the
+ // right trade for an instrument.
+ const n = try port.readTimeout(&buf, 0);
if (n == 0) continue;
if (first < 0) first = nowUs(port);
bytes += n;