1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
|
//! 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;
|