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
|
//! LEDC's side of the differential test: every operation expressed as ESP-IDF's LL calls and as this
//! project's HAL calls.
//!
//! **A register diff cannot prove that a commit happened.** `LEDC_PARA_UP_CHn` and
//! `LEDC_TIMERn_PARA_UP` are write-to-trigger bits that the hardware clears again by itself, so the
//! word that carried the commit reads back exactly as it did before, and the shadow registers the
//! commit copies into are not addressable. Two snapshots therefore agree whether or not either
//! implementation committed anything at all. Nothing in this file claims otherwise.
//!
//! What the diff *can* prove, and what these cases are shaped to prove:
//!
//! * The **staged values** match. Every case stages through the same fields IDF's LL stages, so a
//! wrong shift, a wrong instance stride or a `write` where a `modify` was needed shows up in the
//! staged word - which is the register the commit will read.
//! * The commit **did not destroy the staging**. This is the real hazard of a commit bit that lives
//! inside the word it commits: `LEDC_PARA_UP_CH0` is bit 4 of `LEDC_CH0_CONF0_REG`, so a commit
//! implemented as `writeRaw(1 << 4)` would zero `TIMER_SEL`, `SIG_OUT_EN`, `IDLE_LV` and
//! `OVF_NUM` on its way past. That failure is loud here: the staged word would differ.
//! * `stage_without_commit` pins the distinction down. It stages a duty and stops, on both sides.
//! It must pass, and it must pass for the same reason a committed case passes - which is the
//! evidence that "passes" says nothing about the commit.
//!
//! `LEDC_CHn_DUTY_R_REG` is the one register that reflects the committed shadow rather than the
//! staged value, and it is listed as volatile below rather than used as proof: it updates when the
//! timer next overflows, so what it holds at snapshot time depends on where the counter happened to
//! be. Proving the commit needs an oscilloscope, or the ovf-count interrupt, not a register read.
//!
//! Four windows, because LEDC's state is not in one place: the peripheral block, its gamma RAM
//! aperture, the GPIO matrix (pin routing touches no LEDC register at all) and HP_SYS_CLKRST (where
//! the P4 moved LEDC's clock mux). One suite each, since a `Peripheral` descriptor is one contiguous
//! range of words.
const std = @import("std");
const hal = @import("hal");
const regs = @import("regs");
const mmio = @import("mmio");
const types = @import("differ_types.zig");
const ledc = hal.ledc;
extern fn oracle_ledc_enable_function_clock(enable: c_int) void;
extern fn oracle_ledc_set_clock_source(sel: c_uint) void;
extern fn oracle_ledc_divisor(src_clk_freq: c_uint, freq_hz: c_int, precision: c_uint) c_uint;
extern fn oracle_ledc_set_clock_divider(timer: c_uint, div: c_uint) void;
extern fn oracle_ledc_set_duty_resolution(timer: c_uint, bits: c_uint) void;
extern fn oracle_ledc_commit_timer(timer: c_uint) void;
extern fn oracle_ledc_reset_timer(timer: c_uint) void;
extern fn oracle_ledc_pause_timer(timer: c_uint) void;
extern fn oracle_ledc_resume_timer(timer: c_uint) void;
extern fn oracle_ledc_configure_timer(timer: c_uint, src_hz: c_uint, freq_hz: c_int, resolution: c_uint) void;
extern fn oracle_ledc_bind_timer(channel: c_uint, timer: c_uint) void;
extern fn oracle_ledc_set_hpoint(channel: c_uint, hpoint: c_uint) void;
extern fn oracle_ledc_set_duty(channel: c_uint, duty: c_uint) void;
extern fn oracle_ledc_set_output_enabled(channel: c_uint, enable: c_int) void;
extern fn oracle_ledc_set_idle_level(channel: c_uint, level: c_uint) void;
extern fn oracle_ledc_commit_channel(channel: c_uint) void;
extern fn oracle_ledc_start(channel: c_uint) void;
extern fn oracle_ledc_stop(channel: c_uint, idle_level: c_uint) void;
extern fn oracle_ledc_configure_channel(
channel: c_uint,
timer: c_uint,
duty: c_uint,
hpoint: c_uint,
idle_level: c_uint,
output_enabled: c_int,
) void;
extern fn oracle_ledc_set_pin(pin: c_uint, channel: c_uint) void;
/// The channel, timer and pad under test. Module-level variables because Zig has no closures and the
/// harness stores plain `fn` pointers; the alternative, a comptime-specialised pair per channel,
/// would compare code this project does not ship.
///
/// The suite is safe to run once per pair, the way GPIO's is run once per pin - `channels` and
/// `timers` name the pairs worth using: instance 0, and the far end of each range, where a wrong
/// `RegArray` stride would land outside the block.
pub var channel: u32 = 0;
pub var timer: u32 = 0;
/// GPIO33 is a free pin on this board's JP1 header. GPIO20 is the LED, which the harness itself
/// leaves blinking, and GPIO54 is the ESP32-C6's reset line and must never be driven.
pub var pin: u8 = 33;
pub const channels = [_]u32{ 0, 7 };
pub const timers = [_]u32{ 0, 3 };
/// 40 MHz XTAL: `ClockSource.xtal.hz()`, and what `setup` selects. Passed explicitly to both sides
/// so the two arithmetics are compared on the same input rather than on each side's idea of the
/// clock tree.
const src_hz: u32 = ledc.xtal_hz;
/// Bring LEDC up before the first case: its APB gate is off at power-on, so without this every
/// snapshot would be the last value the bus latched and the harness would (correctly) skip the whole
/// suite on the `clock` check.
fn setup() void {
ledc.init(.xtal);
}
// ------------------------------------------------------------------- the peripheral block itself
pub const suite: types.Suite = .{
.descriptor = .{
.name = "ledc",
.base = @intCast(regs.LEDC_CH0_CONF0_REG),
// 96 words, 0x000-0x17f: eight channels (0x000-0x09f), four timers (0x0a0-0x0bf), the
// interrupt registers, the per-channel gamma *configuration* at 0x100-0x11f (the range
// count lives there, and `setDuty` writes it), the ETM enables, the timer compare and
// capture registers, and LEDC_CONF/LEDC_DATE at 0x170/0x174. Wide enough that every
// register any operation in this file touches is inside it except the gamma RAM aperture at
// 0x400, which has its own suite below.
//
// The reserved gaps (0x0d0-0x0ff, 0x130-0x13f, 0x160-0x16f) are read as well, deliberately:
// if a reserved word does not read back stably the diff will name the offset instead of
// hiding it.
.words = 96,
.volatile_words = &.{
// LEDC_CHn_DUTY_R: the committed duty shadow, reloaded on timer overflow.
(0x010 - 0x000) / 4, (0x024 - 0x000) / 4, (0x038 - 0x000) / 4, (0x04c - 0x000) / 4,
(0x060 - 0x000) / 4, (0x074 - 0x000) / 4, (0x088 - 0x000) / 4, (0x09c - 0x000) / 4,
// LEDC_TIMERn_VALUE: the live counters.
(0x0a4 - 0x000) / 4, (0x0ac - 0x000) / 4, (0x0b4 - 0x000) / 4, (0x0bc - 0x000) / 4,
// LEDC_INT_RAW and LEDC_INT_ST: overflow and fade-end bits latch while the timers run.
(0x0c0 - 0x000) / 4, (0x0c4 - 0x000) / 4,
// LEDC_TIMERn_CNT_CAP: captured counter values.
(0x150 - 0x000) / 4, (0x154 - 0x000) / 4, (0x158 - 0x000) / 4, (0x15c - 0x000) / 4,
},
// REG_LEDC_APB_CLK_EN, bit 0 of SOC_CLK_CTRL3 (ledc_ll.h:135). Its reset value is 0, so this
// check is not a formality for LEDC: it is the difference between a snapshot and a memory of
// one.
.clock = .{
.reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL3_REG),
.bit = @intCast(regs.HP_SYS_CLKRST_REG_LEDC_APB_CLK_EN_S),
},
// The peripheral reset, REG_RST_EN_LEDC, bit 29 of HP_RST_EN1 (ledc_ll.h:150,
// hp_sys_clkrst_reg.h:3497-3503). Sound here where a configure-restore would not be: this
// block has write-to-trigger fields (both PARA_UPs, OVF_CNT_RESET) whose reset value is only
// defined by the reset, and `LEDC_TIMERn_RST` is one of the fields whose reset value is 1 -
// so "write zeros everywhere" would not be a restore at all. Measured safe on this board:
// pulsing it for 1 ms left the console untouched and returned LEDC_CH0_CONF0 to 0.
.restore = .{ .reset_bit = .{
.reg = @intCast(regs.HP_SYS_CLKRST_HP_RST_EN1_REG),
.bit = @intCast(regs.HP_SYS_CLKRST_REG_RST_EN_LEDC_S),
} },
},
.setup = setup,
.cases = &.{
// Timer: the whole sequence, at four target frequencies across three duty resolutions. Each
// side computes its own divider - IDF's `ledc_calculate_divisor`, ours `hal.ledc.divisor` -
// so a mismatch in the fixed-point arithmetic lands in LEDC_TIMERn_CONF[22:5] and is caught
// here rather than being argued about. The four dividers are 1250, 500, 2000 and 2083.
.{ .name = "configure_timer_1kHz_13bit", .arg = 1_000, .idf = idfTimer1k13, .ours = ourTimer1k13 },
.{ .name = "configure_timer_20kHz_10bit", .arg = 20_000, .idf = idfTimer20k10, .ours = ourTimer20k10 },
.{ .name = "configure_timer_5kHz_10bit", .arg = 5_000, .idf = idfTimer5k10, .ours = ourTimer5k10 },
.{ .name = "configure_timer_300Hz_14bit", .arg = 300, .idf = idfTimer300_14, .ours = ourTimer300_14 },
// The divider store and the arithmetic behind it, without the resolution/resume/reset tail.
.{ .name = "clock_divider_only", .arg = 1_250, .idf = idfDivider, .ours = ourDivider },
.{ .name = "duty_resolution_only", .arg = 13, .idf = idfResolution, .ours = ourResolution },
.{ .name = "timer_pause", .idf = idfPause, .ours = ourPause },
.{ .name = "timer_resume", .idf = idfResume, .ours = ourResume },
.{ .name = "timer_reset", .idf = idfTimerReset, .ours = ourTimerReset },
// Channel.
.{ .name = "bind_timer", .idf = idfBind, .ours = ourBind },
.{ .name = "set_hpoint", .arg = 0x400, .idf = idfHpoint, .ours = ourHpoint },
.{ .name = "set_duty", .arg = 0x1000, .idf = idfDuty4096, .ours = ourDuty4096 },
.{ .name = "set_duty", .arg = 0, .idf = idfDuty0, .ours = ourDuty0 },
// Staged and left uncommitted, on both sides. Passes for the same reason the committed cases
// pass, which is the point: the commit is not in the picture the harness takes.
.{ .name = "stage_without_commit", .arg = 0x555, .idf = idfStageOnly, .ours = ourStageOnly },
.{ .name = "channel_start", .idf = idfStart, .ours = ourStart },
.{ .name = "channel_stop_idle_low", .arg = 0, .idf = idfStopLow, .ours = ourStopLow },
.{ .name = "channel_stop_idle_high", .arg = 1, .idf = idfStopHigh, .ours = ourStopHigh },
.{ .name = "configure_channel", .arg = 0x800, .idf = idfConfigureChannel, .ours = ourConfigureChannel },
.{ .name = "full_rf_config_25MHz_1bit", .arg = 25, .idf = idfFullRf, .ours = ourFullRf },
},
};
// -------------------------------------------------------------------------- the gamma RAM window
/// Zero the whole gamma RAM aperture and pulse the peripheral reset.
///
/// The zeroing is the load-bearing half. Gamma RAM is RAM: the peripheral reset does *not* clear it,
/// so without this the second run would inherit whatever the first run wrote, and an implementation
/// that wrote no gamma entry at all would compare equal to one that did - the self-consistent test
/// that proves nothing. All 128 words rather than the channel under test's 16, so that the state the
/// two runs start from does not depend on which cases ran before.
fn restoreGamma() void {
var w: u32 = 0;
while (w < 128) : (w += 1) {
mmio.Reg.atAddress(@as(u32, @intCast(regs.LEDC_CH0_GAMMA_RANGE0_REG)) + 4 * w).writeRaw(0);
}
hal.clkrst.resetPeripheral(.ledc);
}
/// The gamma RAM aperture, 0x400-0x5ff: sixteen entries for each of the eight channels.
///
/// It has its own suite because it is not contiguous with the register block - between them lies a
/// 0x288-byte hole that nothing documents, and reading unmapped peripheral space to get from one to
/// the other is not a risk worth taking on the only board.
///
/// What it covers: on the P4 a constant duty is a degenerate one-step fade, because
/// `DUTY_NUM`/`DUTY_CYCLE`/`DUTY_SCALE`/`DUTY_INC` moved out of `LEDC_CHn_CONF1_REG` into this RAM.
/// `setDuty` writes entry 0 accordingly (ledc.c:263-280), and this is the window that sees it.
pub const gamma_suite: types.Suite = .{
.descriptor = .{
.name = "ledc_gamma",
.base = @intCast(regs.LEDC_CH0_GAMMA_RANGE0_REG),
.words = 128,
.clock = .{
.reg = @intCast(regs.HP_SYS_CLKRST_SOC_CLK_CTRL3_REG),
.bit = @intCast(regs.HP_SYS_CLKRST_REG_LEDC_APB_CLK_EN_S),
},
.restore = .{ .configure = restoreGamma },
},
.setup = setup,
.cases = &.{
.{ .name = "set_duty_writes_entry0", .arg = 0x1000, .idf = idfDuty4096, .ours = ourDuty4096 },
.{ .name = "set_duty_writes_entry0", .arg = 0, .idf = idfDuty0, .ours = ourDuty0 },
.{ .name = "configure_channel_writes_entry0", .arg = 0x800, .idf = idfConfigureChannel, .ours = ourConfigureChannel },
},
};
// ------------------------------------------------------------------------------- the GPIO window
/// The pad back to a known state: driver off, IO MUX word zeroed, matrix pointing at plain GPIO.
/// The same restore GPIO's own suite uses, for the same reason - there is no reset bit for GPIO and
/// the pads are the board's wiring.
fn restorePad() void {
hal.gpio.outputDisable(pin);
mmio.Reg.atAddress(@as(u32, @intCast(regs.PERIPHS_IO_MUX_U_PAD_GPIO0)) + 4 * @as(u32, pin)).writeRaw(0);
mmio.Reg.atAddress(@as(u32, @intCast(regs.GPIO_FUNC0_OUT_SEL_CFG_REG)) + 4 * @as(u32, pin))
.writeRaw(hal.gpio.matrix_gpio_signal);
hal.gpio.setLow(pin);
}
/// Pin routing touches no LEDC register: the peripheral has no pad of its own, and `attachPin` is
/// entirely a GPIO matrix operation. So it is compared in the GPIO window, where its effect is - and
/// what is actually under test here is the signal index, `LEDC_LS_SIG_OUT_PAD_OUT0_IDX + channel`,
/// which is the one piece of arithmetic in the routing path.
pub const routing_suite: types.Suite = .{
.descriptor = .{
.name = "ledc_pin",
.base = @intCast(regs.GPIO_OUT_REG - 4), // GPIO_BT_SELECT_REG sits at +0x00
.words = 400,
.volatile_words = &.{
(0x03c - 0x000) / 4, // GPIO_IN - the outside world, which moves
(0x040 - 0x000) / 4, // GPIO_IN1
},
.restore = .{ .configure = restorePad },
},
.cases = &.{
.{ .name = "attach_pin", .idf = idfAttachPin, .ours = ourAttachPin },
.{ .name = "attach_pin_channel7", .arg = 7, .idf = idfAttachPin7, .ours = ourAttachPin7 },
},
};
// -------------------------------------------------------------------------- the HP_SYS_CLKRST word
/// LEDC's clock mux and function-clock gate back to what `setup` establishes. Only LEDC's own fields
/// are written: PERI_CLK_CTRL22 also holds RMT's, and this is a live board.
/// Restored through ESP-IDF's side, never through the code under test. `differ.zig` runs restore,
/// idf, snapshot, restore, ours, snapshot: with the HAL on both the restore and the "ours" side, a
/// HAL function that does nothing leaves run B's snapshot equal to run A's and the case passes. That
/// makes a suite blind to precisely the failure it was written to catch.
fn restoreClk() void {
oracle_ledc_set_clock_source(0); // 0 = XTAL, the value idfSrcXtal uses
oracle_ledc_enable_function_clock(1);
}
/// One word: `HP_SYS_CLKRST_PERI_CLK_CTRL22_REG`, which on the P4 holds LEDC's clock source select
/// and its function-clock gate (ledc_ll.h:179, :241). This is where the LEDC clock source lives on
/// this die - not in `LEDC_CONF_REG.APB_CLK_SEL`, which the register map still documents with a
/// *different* encoding and which IDF's P4 LL never writes. A HAL that wrote the in-block register
/// would pass every case in the `ledc` suite above and produce no PWM at all; this window is what
/// makes that visible.
///
/// The case order matters: the last case must leave the function clock on and the source at XTAL,
/// because the harness restores *before* each case and not after the last one.
pub const clock_suite: types.Suite = .{
.descriptor = .{
.name = "ledc_clk",
.base = @intCast(regs.HP_SYS_CLKRST_PERI_CLK_CTRL22_REG),
.words = 1,
.restore = .{ .configure = restoreClk },
},
.setup = setup,
.cases = &.{
.{ .name = "clock_source_rc_fast", .arg = 1, .idf = idfSrcRcFast, .ours = ourSrcRcFast },
.{ .name = "clock_source_pll_div", .arg = 2, .idf = idfSrcPllDiv, .ours = ourSrcPllDiv },
.{ .name = "clock_source_xtal", .arg = 0, .idf = idfSrcXtal, .ours = ourSrcXtal },
.{ .name = "function_clock_off", .arg = 0, .idf = idfFuncClkOff, .ours = ourFuncClkOff },
.{ .name = "function_clock_on", .arg = 1, .idf = idfFuncClkOn, .ours = ourFuncClkOn },
},
};
/// All four windows, in the order they should run: the block first, because a failure there explains
/// failures in the other three.
pub const suites = [_]types.Suite{ suite, gamma_suite, routing_suite, clock_suite };
// --------------------------------------------------------------------------------- the case pairs
//
// `catch {}` rather than `catch unreachable` on the `configureTimer` calls: all four divider values
// are inside the field's range (checked on the host against IDF's own expression), so the error path
// is dead - but if this HAL's validity check ever disagreed with IDF's, doing nothing leaves the
// timer unconfigured and the harness reports a diff, where `unreachable` would be undefined
// behaviour in a ReleaseSmall build and would report nothing.
fn idfTimer1k13() void {
oracle_ledc_configure_timer(timer, src_hz, 1_000, 13);
}
fn ourTimer1k13() void {
ledc.configureTimer(timer, .{ .src_hz = src_hz, .freq_hz = 1_000, .resolution = 13 }) catch {};
}
fn idfTimer20k10() void {
oracle_ledc_configure_timer(timer, src_hz, 20_000, 10);
}
fn ourTimer20k10() void {
ledc.configureTimer(timer, .{ .src_hz = src_hz, .freq_hz = 20_000, .resolution = 10 }) catch {};
}
fn idfTimer5k10() void {
oracle_ledc_configure_timer(timer, src_hz, 5_000, 10);
}
fn ourTimer5k10() void {
ledc.configureTimer(timer, .{ .src_hz = src_hz, .freq_hz = 5_000, .resolution = 10 }) catch {};
}
fn idfTimer300_14() void {
oracle_ledc_configure_timer(timer, src_hz, 300, 14);
}
fn ourTimer300_14() void {
ledc.configureTimer(timer, .{ .src_hz = src_hz, .freq_hz = 300, .resolution = 14 }) catch {};
}
// Each side computes the divider with its own arithmetic and stores it with its own code: 40 MHz,
// 1 kHz, 13 bits, which is 1250 = 0x4E2 = 4.8828 in Q10.8.
fn idfDivider() void {
oracle_ledc_set_clock_divider(timer, oracle_ledc_divisor(src_hz, 1_000, 1 << 13));
oracle_ledc_commit_timer(timer);
}
fn ourDivider() void {
ledc.setClockDivider(timer, ledc.divisor(src_hz, 1_000, 13));
ledc.commitTimer(timer);
}
fn idfResolution() void {
oracle_ledc_set_duty_resolution(timer, 13);
oracle_ledc_commit_timer(timer);
}
fn ourResolution() void {
ledc.setDutyResolution(timer, 13);
ledc.commitTimer(timer);
}
fn idfPause() void {
oracle_ledc_pause_timer(timer);
}
fn ourPause() void {
ledc.pauseTimer(timer);
}
fn idfResume() void {
oracle_ledc_resume_timer(timer);
}
fn ourResume() void {
ledc.resumeTimer(timer);
}
fn idfTimerReset() void {
oracle_ledc_reset_timer(timer);
}
fn ourTimerReset() void {
ledc.resetTimer(timer);
}
fn idfBind() void {
oracle_ledc_bind_timer(channel, timer);
oracle_ledc_commit_channel(channel);
}
fn ourBind() void {
ledc.bindTimer(channel, timer);
ledc.commitChannel(channel);
}
fn idfHpoint() void {
oracle_ledc_set_hpoint(channel, 0x400);
oracle_ledc_commit_channel(channel);
}
fn ourHpoint() void {
ledc.setHpoint(channel, 0x400);
ledc.commitChannel(channel);
}
fn idfDuty4096() void {
oracle_ledc_set_duty(channel, 0x1000);
oracle_ledc_commit_channel(channel);
}
fn ourDuty4096() void {
ledc.setDuty(channel, 0x1000);
ledc.commitChannel(channel);
}
fn idfDuty0() void {
oracle_ledc_set_duty(channel, 0);
oracle_ledc_commit_channel(channel);
}
fn ourDuty0() void {
ledc.setDuty(channel, 0);
ledc.commitChannel(channel);
}
// No commit on either side. The staged duty and gamma entry must still match.
fn idfStageOnly() void {
oracle_ledc_set_duty(channel, 0x555);
}
fn ourStageOnly() void {
ledc.setDuty(channel, 0x555);
}
fn idfStart() void {
oracle_ledc_start(channel);
}
fn ourStart() void {
ledc.start(channel);
}
fn idfStopLow() void {
oracle_ledc_stop(channel, 0);
}
fn ourStopLow() void {
ledc.stop(channel, 0);
}
fn idfStopHigh() void {
oracle_ledc_stop(channel, 1);
}
fn ourStopHigh() void {
ledc.stop(channel, 1);
}
fn idfConfigureChannel() void {
oracle_ledc_configure_channel(channel, timer, 0x800, 0x200, 1, 1);
}
fn ourConfigureChannel() void {
ledc.configureChannel(channel, .{
.timer = timer,
.duty = 0x800,
.hpoint = 0x200,
.idle_level = 1,
.output_enabled = true,
});
}
fn idfAttachPin() void {
oracle_ledc_set_pin(pin, channel);
}
fn ourAttachPin() void {
ledc.attachPin(channel, pin);
}
// Channel 7 explicitly, because the signal index is arithmetic on the channel number and 0 is the
// one value that cannot catch an off-by-one in it.
fn idfAttachPin7() void {
oracle_ledc_set_pin(pin, 7);
}
fn ourAttachPin7() void {
ledc.attachPin(7, pin);
}
/// The report's RF configuration, end to end: 1-bit resolution at 25 MHz, duty 1, hpoint 0, on
/// channel 0 / timer 0. Reproduced from the ESP-IDF firmware in 02-esp32p4-m3-radio/main/main.c:77-94.
///
/// This case exists because the two implementations disagree *on the die* for exactly this
/// configuration and nothing smaller: the IDF firmware's carrier toggles GPIO20 at 25 MHz (proven by
/// its own ADC witness catching both rails), and this project's HAL leaves the pad static, while
/// every individual register operation compares equal. So the difference is in the composition, and
/// comparing the whole block after each full bring-up is the only thing that can localise it.
/// Note the source: 80 MHz, not this suite's default `src_hz` (which is XTAL at 40 MHz). At 40 MHz a
/// 1-bit 25 MHz target needs divider 205, below the legal minimum of 256, and the two sides then
/// disagree for a reason that has nothing to do with the RF experiment: this HAL rejects it with
/// DividerOutOfRange while ESP-IDF's *LL* programs it anyway, because IDF's range check lives one
/// layer up in ledc.c rather than in the LL. Worth knowing - it means an IDF LL caller can silently
/// program an illegal divider - but it is not what this case is for.
fn idfFullRf() void {
oracle_ledc_configure_timer(0, ledc.pll_div_hz, 25_000_000, 1);
oracle_ledc_configure_channel(0, 0, 1, 0, 0, 1);
}
fn ourFullRf() void {
ledc.configureTimer(0, .{ .src_hz = ledc.pll_div_hz, .freq_hz = 25_000_000, .resolution = 1 }) catch return;
ledc.configureChannel(0, .{ .timer = 0, .duty = 1, .hpoint = 0, .idle_level = 0 });
}
fn idfSrcXtal() void {
oracle_ledc_set_clock_source(0);
}
fn ourSrcXtal() void {
ledc.setClockSource(.xtal);
}
fn idfSrcRcFast() void {
oracle_ledc_set_clock_source(1);
}
fn ourSrcRcFast() void {
ledc.setClockSource(.rc_fast);
}
fn idfSrcPllDiv() void {
oracle_ledc_set_clock_source(2);
}
fn ourSrcPllDiv() void {
ledc.setClockSource(.pll_div);
}
fn idfFuncClkOff() void {
oracle_ledc_enable_function_clock(0);
}
fn ourFuncClkOff() void {
ledc.setFunctionClockEnabled(false);
}
fn idfFuncClkOn() void {
oracle_ledc_enable_function_clock(1);
}
fn ourFuncClkOn() void {
ledc.setFunctionClockEnabled(true);
}
|