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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
|
//! The timer groups: TIMG0 and TIMG1, each two general-purpose 54-bit timers plus one MWDT.
//!
//! Three unrelated functions share one register block (timg_ll.h:7 says so in as many words):
//! the general-purpose timers, the main watchdog, and RTC clock calibration. Only the first two are
//! here; calibration belongs to the clock tree, and ETM and interrupts are deliberately absent.
//!
//! Four things about this block cost real care, all of them taken from ESP-IDF's LL rather than
//! guessed at:
//!
//! **Reading the counter is a sequence, not a load** (timer_ll.h:248-269). The counter lives in a
//! different clock domain from the register file, so its value only appears in `TxLO`/`TxHI` after a
//! software capture:
//!
//! write TIMG_TxUPDATE = 1 -> ask for a capture
//! poll until TIMG_Tx_UPDATE == 0 -> the hardware clears it when the pair is latched
//! read TxHI, then TxLO -> 22 bits + 32 bits = the 54-bit count
//!
//! Note the polarity: unlike SYSTIMER, which sets a separate `VALUE_VALID` bit, this peripheral
//! *clears the request bit* to acknowledge. Waiting for it to become 1 hangs forever; not waiting at
//! all returns whatever the last capture left, which for a never-captured timer is 0 and therefore
//! looks like a stopped timer rather than like a bug.
//!
//! **The watchdog registers are write-protected, and the key is the reset value** (mwdt_ll.h:231-244
//! and timer_group_reg.h, TIMG_WDT_WKEY: "If the register contains a different value than its reset
//! value, write protection is enabled", default 1356348065 = 0x50D83AA1). So "unlock" means writing
//! the key back, and "lock" means writing anything else - IDF writes 0. A watchdog register write
//! made while locked is silently dropped, which is the failure mode this file's API shape exists to
//! prevent: every MWDT operation is a method on the `Watchdog` handle returned by `unlock`, and
//! there is no way to reach one without holding it:
//!
//! const wdt = timg.unlock(.timg1);
//! defer wdt.release();
//! wdt.setStage(.stage0, 2_000_000, .reset_system);
//!
//! **Watchdog configuration is committed asynchronously.** Every write to WDTCONFIG0-5 has to be
//! followed by `WDT_CONF_UPDATE_EN` (mwdt_ll.h:122, and again after every other config write), which
//! is a write-to-trigger bit. The exception is `WDT_EN` itself: `mwdt_ll_enable`/`_disable`
//! (mwdt_ll.h:61-77) do *not* pulse it, so neither does `setEnabled` - matching IDF exactly matters
//! more here than consistency, because the differential harness compares the resulting word.
//!
//! **Do not resurrect a watchdog you are not feeding.** TIMG0 hosts MWDT0, which this image's
//! bootloader has already disabled, and `TIMG_WDT_FLASHBOOT_MOD_EN` defaults to 1 and runs the
//! watchdog *independently of* `WDT_EN` (mwdt_ll.h:186-188). Resetting a timer group therefore
//! re-arms flash-boot protection and reboots the board a moment later with nothing on the console to
//! explain it; `clkrst.resetPeripheral` clears the bit as part of the reset for exactly this reason
//! (its `clears_flashboot` flag), which is why nothing in this file pulses a reset bit itself.
const std = @import("std");
const regs = @import("regs");
const mmio = @import("mmio");
const clkrst = @import("clkrst.zig");
const Reg = mmio.Reg;
const Field = mmio.Field;
/// TIMG_LL_INST_NUM (timg_ll.h:20).
pub const group_count = 2;
/// TIMG_LL_GPTIMERS_PER_INST (timg_ll.h:23). Two per group on the P4, unlike the C-series parts.
pub const timers_per_group = 2;
/// TIMER_LL_COUNTER_BIT_WIDTH (timer_ll.h:25). 32 bits in `TxLO` plus 22 in `TxHI`.
pub const counter_bits = 54;
pub const Group = enum(u1) { timg0 = 0, timg1 = 1 };
pub const Timer = enum(u1) { t0 = 0, t1 = 1 };
// -------------------------------------------------------------------------------- addressing
//
// The macros are indexed two different ways at once and neither is derivable from the other:
// `TIMG_T0CONFIG_REG(i)` takes the *group*, while the *timer* is baked into the macro name
// (`T0CONFIG` vs `T1CONFIG`). Rather than duplicate every accessor per timer, the timer index is
// turned into a stride - but a stride assumed is a stride that eventually writes into the next
// register, so both strides are checked at comptime against the macros for the other instance.
const group_stride = mmio.addr(regs.TIMG_T0CONFIG_REG(1)) - mmio.addr(regs.TIMG_T0CONFIG_REG(0));
const timer_stride = mmio.addr(regs.TIMG_T1CONFIG_REG(0)) - mmio.addr(regs.TIMG_T0CONFIG_REG(0));
// Absolute addresses of group 0 / timer 0's registers. Every other (group, timer) is these plus a
// multiple of the two strides.
const a_config = mmio.addr(regs.TIMG_T0CONFIG_REG(0));
const a_lo = mmio.addr(regs.TIMG_T0LO_REG(0));
const a_hi = mmio.addr(regs.TIMG_T0HI_REG(0));
const a_update = mmio.addr(regs.TIMG_T0UPDATE_REG(0));
const a_alarm_lo = mmio.addr(regs.TIMG_T0ALARMLO_REG(0));
const a_alarm_hi = mmio.addr(regs.TIMG_T0ALARMHI_REG(0));
const a_load_lo = mmio.addr(regs.TIMG_T0LOADLO_REG(0));
const a_load_hi = mmio.addr(regs.TIMG_T0LOADHI_REG(0));
const a_load = mmio.addr(regs.TIMG_T0LOAD_REG(0));
comptime {
// The timer sub-block is contiguous and uniform - assert it, per register, rather than trust
// that 0x24 happens to be right for all nine.
const pairs = .{
.{ a_config, mmio.addr(regs.TIMG_T1CONFIG_REG(0)) },
.{ a_lo, mmio.addr(regs.TIMG_T1LO_REG(0)) },
.{ a_hi, mmio.addr(regs.TIMG_T1HI_REG(0)) },
.{ a_update, mmio.addr(regs.TIMG_T1UPDATE_REG(0)) },
.{ a_alarm_lo, mmio.addr(regs.TIMG_T1ALARMLO_REG(0)) },
.{ a_alarm_hi, mmio.addr(regs.TIMG_T1ALARMHI_REG(0)) },
.{ a_load_lo, mmio.addr(regs.TIMG_T1LOADLO_REG(0)) },
.{ a_load_hi, mmio.addr(regs.TIMG_T1LOADHI_REG(0)) },
.{ a_load, mmio.addr(regs.TIMG_T1LOAD_REG(0)) },
};
for (pairs) |p| {
if (p[1] - p[0] != timer_stride) @compileError(
"the two timers' registers are not a uniform stride apart; index them per timer",
);
}
// And the group stride is the same for a register other than CONFIG.
if (mmio.addr(regs.TIMG_T0LO_REG(1)) - a_lo != group_stride)
@compileError("the two timer groups are not a uniform stride apart");
// T0's and T1's *fields* sit at the same bit positions in their respective registers, which is
// what makes one set of Field constants enough. If a future register set moves one of them,
// this stops the build instead of writing the divider into the alarm enable.
const t1_divider = Field.of(regs.TIMG_T1_DIVIDER_S, regs.TIMG_T1_DIVIDER_V);
const t1_en = Field.of(regs.TIMG_T1_EN_S, regs.TIMG_T1_EN_V);
const t1_update = Field.of(regs.TIMG_T1_UPDATE_S, regs.TIMG_T1_UPDATE_V);
const t1_hi = Field.of(regs.TIMG_T1_HI_S, regs.TIMG_T1_HI_V);
if (t1_divider.shift != divider.shift or t1_divider.width != divider.width or
t1_en.shift != counter_en.shift or t1_update.shift != update.shift or
t1_hi.width != count_hi.width)
@compileError("timer 0 and timer 1 disagree on field positions; look up fields per timer");
}
inline fn tReg(comptime a0: u32, g: Group, t: Timer) Reg {
return Reg.atAddress(a0 +
group_stride * @as(u32, @intFromEnum(g)) +
timer_stride * @as(u32, @intFromEnum(t)));
}
/// `a0` is not comptime: the stage-timeout registers are picked by a runtime `Stage`
/// (`stageHoldAddr`), and every other caller passes a constant that folds anyway.
inline fn gReg(a0: u32, g: Group) Reg {
return Reg.atAddress(a0 + group_stride * @as(u32, @intFromEnum(g)));
}
// TxCONFIG fields. `divcnt_rst` is write-to-trigger; the rest are plain R/W.
const alarm_en = Field.of(regs.TIMG_T0_ALARM_EN_S, regs.TIMG_T0_ALARM_EN_V);
const divcnt_rst = Field.of(regs.TIMG_T0_DIVCNT_RST_S, regs.TIMG_T0_DIVCNT_RST_V);
const divider = Field.of(regs.TIMG_T0_DIVIDER_S, regs.TIMG_T0_DIVIDER_V);
const autoreload = Field.of(regs.TIMG_T0_AUTORELOAD_S, regs.TIMG_T0_AUTORELOAD_V);
const increase = Field.of(regs.TIMG_T0_INCREASE_S, regs.TIMG_T0_INCREASE_V);
const counter_en = Field.of(regs.TIMG_T0_EN_S, regs.TIMG_T0_EN_V);
const update = Field.of(regs.TIMG_T0_UPDATE_S, regs.TIMG_T0_UPDATE_V);
const count_hi = Field.of(regs.TIMG_T0_HI_S, regs.TIMG_T0_HI_V);
const alarm_value_hi = Field.of(regs.TIMG_T0_ALARM_HI_S, regs.TIMG_T0_ALARM_HI_V);
const load_value_hi = Field.of(regs.TIMG_T0_LOAD_HI_S, regs.TIMG_T0_LOAD_HI_V);
// ------------------------------------------------------------------------------ timer clocks
//
// The timers' function clock is selected and gated in HP_SYS_CLKRST, not in the timer group: group 0
// in PERI_CLK_CTRL20 and group 1 in PERI_CLK_CTRL21 (timer_ll.h:117-129, :146-160). Two shared
// registers, so both operations take the interrupt guard - the same read-modify-write hazard
// `clkrst` exists for.
const peri_clk_ctrl20 = Reg.at(regs.HP_SYS_CLKRST_PERI_CLK_CTRL20_REG);
const peri_clk_ctrl21 = Reg.at(regs.HP_SYS_CLKRST_PERI_CLK_CTRL21_REG);
/// The three function clocks a GP timer can run from, with the encodings from
/// `timer_ll_set_clock_source` (timer_ll.h:100-116). The numbering is not the enum order anyone
/// would pick: XTAL is 0, RC_FAST is 1, PLL_F80M is 2.
pub const ClockSource = enum(u2) {
xtal = 0,
rc_fast = 1,
pll_f80m = 2,
};
/// Where the group/timer's source-select and gate fields live. Both are in one word per group, and
/// the bit positions differ per timer, so this is a genuine per-instance lookup rather than a stride.
const TimerClock = struct {
reg: Reg,
src_sel: Field,
clk_en: Field,
};
inline fn timerClock(comptime g: Group, comptime t: Timer) TimerClock {
return switch (g) {
.timg0 => switch (t) {
.t0 => .{
.reg = peri_clk_ctrl20,
.src_sel = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP0_T0_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_TIMERGRP0_T0_SRC_SEL_V),
.clk_en = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP0_T0_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TIMERGRP0_T0_CLK_EN_V),
},
.t1 => .{
.reg = peri_clk_ctrl20,
.src_sel = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP0_T1_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_TIMERGRP0_T1_SRC_SEL_V),
.clk_en = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP0_T1_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TIMERGRP0_T1_CLK_EN_V),
},
},
.timg1 => switch (t) {
.t0 => .{
.reg = peri_clk_ctrl21,
.src_sel = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP1_T0_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_TIMERGRP1_T0_SRC_SEL_V),
.clk_en = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP1_T0_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TIMERGRP1_T0_CLK_EN_V),
},
.t1 => .{
.reg = peri_clk_ctrl21,
.src_sel = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP1_T1_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_TIMERGRP1_T1_SRC_SEL_V),
.clk_en = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP1_T1_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TIMERGRP1_T1_CLK_EN_V),
},
},
};
}
/// Select a timer's function clock. Comptime instance because the field pairing really does differ
/// per (group, timer) - four different bit positions in two registers.
pub fn setClockSource(comptime g: Group, comptime t: Timer, src: ClockSource) void {
const c = comptime timerClock(g, t);
const guard = clkrst.maskInterrupts();
defer guard.release();
c.reg.modify(.{c.src_sel.is(@intFromEnum(src))});
}
/// The timer's function-clock gate, distinct from the group's bus clock in `clkrst`. Defaults to 1
/// at power-on (hp_sys_clkrst_reg.h: REG_TIMERGRP0_T0_CLK_EN default 1).
pub fn setClockEnabled(comptime g: Group, comptime t: Timer, on: bool) void {
const c = comptime timerClock(g, t);
const guard = clkrst.maskInterrupts();
defer guard.release();
c.reg.modify(.{c.clk_en.is(@intFromBool(on))});
}
// ------------------------------------------------------------------------ general purpose timer
pub const Direction = enum { up, down };
/// Prescaler on the function clock. 2 is the smallest the hardware accepts and 65536 the largest,
/// encoded as 0 (timer_ll.h:191-199). The divider counter is reset in a second store afterwards,
/// exactly as IDF does it: without that the new divider only takes effect after the old one's
/// current period ends, so the first tick after a change is the wrong length.
pub fn setDivider(g: Group, t: Timer, div: u32) void {
std.debug.assert(div >= 2 and div <= 65536);
const cfg = tReg(a_config, g, t);
cfg.modify(.{divider.is(if (div >= 65536) 0 else div)});
cfg.modify(.{divcnt_rst.is(1)});
}
pub fn setDirection(g: Group, t: Timer, dir: Direction) void {
tReg(a_config, g, t).modify(.{increase.is(@intFromBool(dir == .up))});
}
/// Reload the counter from `TxLOADLO`/`TxLOADHI` automatically on every alarm.
pub fn setAutoReload(g: Group, t: Timer, on: bool) void {
tReg(a_config, g, t).modify(.{autoreload.is(@intFromBool(on))});
}
pub fn setCounterEnabled(g: Group, t: Timer, on: bool) void {
tReg(a_config, g, t).modify(.{counter_en.is(@intFromBool(on))});
}
pub fn setAlarmEnabled(g: Group, t: Timer, on: bool) void {
tReg(a_config, g, t).modify(.{alarm_en.is(@intFromBool(on))});
}
/// The 54-bit alarm value. Low word first would be equally correct - the comparator only sees the
/// pair - but IDF writes high then low (timer_ll.h:279-283) and matching its order keeps the write
/// trace comparable.
pub fn setAlarmValue(g: Group, t: Timer, value: u64) void {
tReg(a_alarm_hi, g, t).modify(.{alarm_value_hi.is(@truncate(value >> 32))});
tReg(a_alarm_lo, g, t).writeRaw(@truncate(value));
}
/// The value a reload puts into the counter, whether triggered by `load` or by an auto-reload.
pub fn setLoadValue(g: Group, t: Timer, value: u64) void {
tReg(a_load_hi, g, t).modify(.{load_value_hi.is(@truncate(value >> 32))});
tReg(a_load_lo, g, t).writeRaw(@truncate(value));
}
pub fn getLoadValue(g: Group, t: Timer) u64 {
const hi: u64 = tReg(a_load_hi, g, t).get(load_value_hi);
return (hi << 32) | tReg(a_load_lo, g, t).raw();
}
/// Copy the load value into the counter now. `TIMG_TxLOAD_REG` is a whole-word write-to-trigger
/// register: the value written is irrelevant, so this is a bare store rather than a field write.
pub fn load(g: Group, t: Timer) void {
tReg(a_load, g, t).writeRaw(1);
}
/// The counter, through the capture handshake described at the top of this file.
///
/// Returns null rather than spinning forever if the peripheral never acknowledges: with the group's
/// bus clock gated off, or the timer's function clock gated off, `UPDATE` never clears, and hanging
/// inside a HAL call with no output is the worst possible way to report that. The bound is the same
/// 10,000 reads `systimer.read` uses.
pub fn read(g: Group, t: Timer) ?u64 {
const upd = tReg(a_update, g, t);
// Ask for a capture. IDF assigns to the struct bitfield, which is a read-modify-write of a word
// whose only other bits are reserved, so `modify` is both the honest operation and the one that
// produces the same store.
upd.modify(.{update.is(1)});
var spins: u32 = 0;
while (upd.get(update) != 0) {
spins += 1;
if (spins > 10_000) return null;
}
const hi: u64 = tReg(a_hi, g, t).get(count_hi);
return (hi << 32) | tReg(a_lo, g, t).raw();
}
// ----------------------------------------------------------------------------------- watchdog
const a_wdtconfig0 = mmio.addr(regs.TIMG_WDTCONFIG0_REG(0));
const a_wdtconfig1 = mmio.addr(regs.TIMG_WDTCONFIG1_REG(0));
const a_wdtconfig2 = mmio.addr(regs.TIMG_WDTCONFIG2_REG(0));
const a_wdtconfig3 = mmio.addr(regs.TIMG_WDTCONFIG3_REG(0));
const a_wdtconfig4 = mmio.addr(regs.TIMG_WDTCONFIG4_REG(0));
const a_wdtconfig5 = mmio.addr(regs.TIMG_WDTCONFIG5_REG(0));
const a_wdtfeed = mmio.addr(regs.TIMG_WDTFEED_REG(0));
const a_wdtwprotect = mmio.addr(regs.TIMG_WDTWPROTECT_REG(0));
const wdt_en = Field.of(regs.TIMG_WDT_EN_S, regs.TIMG_WDT_EN_V);
const wdt_conf_update_en = Field.of(regs.TIMG_WDT_CONF_UPDATE_EN_S, regs.TIMG_WDT_CONF_UPDATE_EN_V);
const wdt_flashboot_mod_en = Field.of(regs.TIMG_WDT_FLASHBOOT_MOD_EN_S, regs.TIMG_WDT_FLASHBOOT_MOD_EN_V);
const wdt_cpu_reset_length = Field.of(regs.TIMG_WDT_CPU_RESET_LENGTH_S, regs.TIMG_WDT_CPU_RESET_LENGTH_V);
const wdt_sys_reset_length = Field.of(regs.TIMG_WDT_SYS_RESET_LENGTH_S, regs.TIMG_WDT_SYS_RESET_LENGTH_V);
const wdt_clk_prescale = Field.of(regs.TIMG_WDT_CLK_PRESCALE_S, regs.TIMG_WDT_CLK_PRESCALE_V);
const wdt_divcnt_rst = Field.of(regs.TIMG_WDT_DIVCNT_RST_S, regs.TIMG_WDT_DIVCNT_RST_V);
/// The write-protect key, and also `TIMG_WDT_WKEY`'s reset value: protection is on whenever the
/// register holds anything *else* (timer_group_reg.h, TIMG_WDT_WKEY, default 1356348065). IDF's
/// `mwdt_ll_write_protect_disable` writes this exact constant (mwdt_ll.h:243).
pub const wkey: u32 = 0x50D8_3AA1;
/// What IDF writes to re-enable protection (mwdt_ll.h:233). Any non-key value would do; using the
/// same one keeps the register comparable against IDF's.
const wkey_locked: u32 = 0;
// The headers carry no reset-value macro to check `wkey` against - `TIMG_WDT_WKEY_V` is the field
// mask, 0xffffffff - so the constant is copied from the two places that state it: the register
// description's "default: 1356348065" and mwdt_ll.h:243's 0x50D83AA1. The unit test at the end of
// this file pins those two against each other, which is the only check available without a chip.
/// MWDT stages, each with its own timeout and its own action. Stage 0 fires first; a stage that is
/// not fed escalates to the next.
pub const Stage = enum(u2) { stage0 = 0, stage1 = 1, stage2 = 2, stage3 = 3 };
/// What a stage does when it expires (mwdt_ll.h:23-26).
pub const Action = enum(u2) {
off = 0,
interrupt = 1,
reset_cpu = 2,
reset_system = 3,
};
/// Length of the reset pulse a `reset_cpu`/`reset_system` stage asserts (mwdt_ll.h:28-35).
pub const ResetLength = enum(u3) {
ns_100 = 0,
ns_200 = 1,
ns_300 = 2,
ns_400 = 3,
ns_500 = 4,
ns_800 = 5,
us_1_6 = 6,
us_3_2 = 7,
};
/// A group's MWDT with write protection lifted, and the only way to reach an MWDT operation:
///
/// const wdt = timg.unlock(.timg1);
/// defer wdt.release();
/// wdt.setStage(.stage0, ticks, .reset_system);
///
/// The handle exists because a watchdog register write made while protection is on is silently
/// dropped - no fault, no status bit, just a watchdog that keeps its old timeout - and that is not a
/// mistake worth making twice.
pub const Watchdog = struct {
group: Group,
/// Re-enable write protection. Not idempotent-with-`unlock` in the composable sense that
/// `clkrst.Guard` is: the hardware has one key register and no nesting count, so an inner
/// `release` really does lock an outer caller out. There is nothing in this HAL that nests.
pub inline fn release(self: Watchdog) void {
gReg(a_wdtwprotect, self.group).writeRaw(wkey_locked);
}
/// WDTCONFIG0-5 are shadowed; the hardware only takes them at a `CONF_UPDATE_EN` pulse
/// (mwdt_ll.h:121-122). Write-to-trigger, so this is a single deliberate store.
inline fn commit(self: Watchdog) void {
gReg(a_wdtconfig0, self.group).modify(.{wdt_conf_update_en.is(1)});
}
inline fn config0(self: Watchdog) Reg {
return gReg(a_wdtconfig0, self.group);
}
/// The stage's action bits and its timeout live in different registers - the action in
/// WDTCONFIG0, the timeout in WDTCONFIG2+stage - which is why this takes both at once
/// (mwdt_ll.h:98-123). `timeout` is in MWDT clock cycles, i.e. after the prescaler.
pub fn setStage(self: Watchdog, stage: Stage, timeout: u32, action: Action) void {
self.config0().modify(.{stageAction(stage).is(@intFromEnum(action))});
gReg(stageHoldAddr(stage), self.group).writeRaw(timeout);
self.commit();
}
/// Turn one stage off without disturbing its timeout (mwdt_ll.h:131-152).
pub fn disableStage(self: Watchdog, stage: Stage) void {
self.config0().modify(.{stageAction(stage).is(@intFromEnum(Action.off))});
self.commit();
}
pub fn getStageTimeout(self: Watchdog, stage: Stage) u32 {
return gReg(stageHoldAddr(stage), self.group).raw();
}
/// Prescaler from the MWDT's source clock (XTAL on this chip - mwdt_ll.h:273-283 asserts it and
/// selects nothing). 1 to 65535; IDF's default is 20000, which gives 500 ticks/us
/// (mwdt_ll.h:20).
pub fn setPrescaler(self: Watchdog, prescaler: u32) void {
std.debug.assert(prescaler >= 1 and prescaler <= 0xffff);
gReg(a_wdtconfig1, self.group).modify(.{wdt_clk_prescale.is(prescaler)});
self.commit();
}
pub fn setCpuResetLength(self: Watchdog, len: ResetLength) void {
self.config0().modify(.{wdt_cpu_reset_length.is(@intFromEnum(len))});
self.commit();
}
pub fn setSysResetLength(self: Watchdog, len: ResetLength) void {
self.config0().modify(.{wdt_sys_reset_length.is(@intFromEnum(len))});
self.commit();
}
/// Flash-boot protection: a second, independent way for this watchdog to run. It ignores
/// `WDT_EN` entirely (mwdt_ll.h:186-188), it defaults to 1, and a group reset re-arms it - so
/// clearing it is part of every sane bring-up, and `clkrst.resetPeripheral` does it.
pub fn setFlashbootEnabled(self: Watchdog, on: bool) void {
self.config0().modify(.{wdt_flashboot_mod_en.is(@intFromBool(on))});
self.commit();
}
/// Start or stop the watchdog. No `CONF_UPDATE_EN` pulse: `mwdt_ll_enable` and `_disable`
/// (mwdt_ll.h:61-77) do not, so neither does this. Disabling does *not* stop flash-boot mode.
pub fn setEnabled(self: Watchdog, on: bool) void {
self.config0().modify(.{wdt_en.is(@intFromBool(on))});
}
pub fn isEnabled(self: Watchdog) bool {
return self.config0().get(wdt_en) == 1;
}
/// Reset the count and the stage. `TIMG_WDTFEED_REG` is a whole-word write-to-trigger register,
/// so the value is irrelevant (mwdt_ll.h:219-222).
pub fn feed(self: Watchdog) void {
gReg(a_wdtfeed, self.group).writeRaw(1);
}
/// Reset the watchdog's clock divider counter. Write-to-trigger, in WDTCONFIG1 alongside the
/// prescaler.
pub fn resetDividerCount(self: Watchdog) void {
gReg(a_wdtconfig1, self.group).modify(.{wdt_divcnt_rst.is(1)});
self.commit();
}
};
/// Lift write protection and hand back the only handle that can touch the MWDT.
pub fn unlock(g: Group) Watchdog {
gReg(a_wdtwprotect, g).writeRaw(wkey);
return .{ .group = g };
}
/// Feed a watchdog, protection dance included. The one MWDT operation that is worth a shortcut,
/// because it is the one called from a loop.
pub fn feed(g: Group) void {
const wdt = unlock(g);
defer wdt.release();
wdt.feed();
}
/// True if write protection is currently on, i.e. the key register holds something other than the
/// key. Reads the register, so it reports the hardware rather than what this module last wrote.
pub fn isWriteProtected(g: Group) bool {
return gReg(a_wdtwprotect, g).raw() != wkey;
}
inline fn stageAction(stage: Stage) Field {
// Stage 0 is at the *top* of the word (bits 30:29) and stage 3 at 24:23, i.e. the stages run
// downwards through the register. The four are a uniform 2 bits apart, but in the reverse of
// the obvious direction, so they are looked up rather than computed.
return switch (stage) {
.stage0 => Field.of(regs.TIMG_WDT_STG0_S, regs.TIMG_WDT_STG0_V),
.stage1 => Field.of(regs.TIMG_WDT_STG1_S, regs.TIMG_WDT_STG1_V),
.stage2 => Field.of(regs.TIMG_WDT_STG2_S, regs.TIMG_WDT_STG2_V),
.stage3 => Field.of(regs.TIMG_WDT_STG3_S, regs.TIMG_WDT_STG3_V),
};
}
inline fn stageHoldAddr(stage: Stage) u32 {
// WDTCONFIG2 holds stage 0's timeout and WDTCONFIG5 stage 3's; the mapping is off by two and
// there is no macro that says so, so it comes from mwdt_ll.h:100-116.
return switch (stage) {
.stage0 => a_wdtconfig2,
.stage1 => a_wdtconfig3,
.stage2 => a_wdtconfig4,
.stage3 => a_wdtconfig5,
};
}
test "the two strides are the documented ones" {
// 0x1000 between groups (timer_group_reg.h:14, REG_TIMG_BASE) and 0x24 between the two timers
// of a group. Both are asserted against the macros at comptime above; this pins the numbers so
// a header change shows up as a failing test with a value in it, not only as a compile error.
try std.testing.expectEqual(@as(u32, 0x1000), group_stride);
try std.testing.expectEqual(@as(u32, 0x24), timer_stride);
}
test "the write-protect key is the register's reset value" {
try std.testing.expectEqual(@as(u32, 1_356_348_065), wkey);
}
|