diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 12:40:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 12:46:51 -0300 |
| commit | f5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch) | |
| tree | 2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/oracle/ledc_cases.zig | |
| download | esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip | |
zig-p4: pure-Zig ESP32-P4 toolchain
build.zig generates the linker script and drives Zig's own LLD; tools/image.zig
turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask
ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker.
src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers;
src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip;
src/oracle/ diffs this HAL against ESP-IDF's on the die.
Diffstat (limited to 'src/oracle/ledc_cases.zig')
| -rw-r--r-- | src/oracle/ledc_cases.zig | 513 |
1 files changed, 513 insertions, 0 deletions
diff --git a/src/oracle/ledc_cases.zig b/src/oracle/ledc_cases.zig new file mode 100644 index 0000000..5a39f83 --- /dev/null +++ b/src/oracle/ledc_cases.zig @@ -0,0 +1,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); +} + |
