//! 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); }