Skip to content

ledc: add esp32c5 support and model ledc_sclk source#4966

Draft
Veanir wants to merge 10 commits intoesp-rs:mainfrom
Veanir:fix/esp32c5-ledc-hil
Draft

ledc: add esp32c5 support and model ledc_sclk source#4966
Veanir wants to merge 10 commits intoesp-rs:mainfrom
Veanir:fix/esp32c5-ledc-hil

Conversation

@Veanir
Copy link
Copy Markdown
Contributor

@Veanir Veanir commented Feb 11, 2026

Summary

  • add ESP32-C5 LEDC support wiring in metadata and HAL - cc ESP32-C5: LEDC peripheral tracking issue #5161
  • model C5 LEDC timer source as LEDC_SCLK and use that for LEDC divider calculations
  • add LEDC HIL loopback tests (duty + output frequency) and include esp32c5 in LEDC HIL matrix

Motivation

On ESP32-C5, LEDC output could be ~2x the configured frequency due to a clock-source mismatch:

  • set_global_slow_clock(LSGlobalClkSource::APBClk) selected the LEDC source as PLL_F80M (80 MHz)
  • divider calculations used apb_clock (40 MHz in C5 defaults)

This PR fixes that mismatch by modeling LEDC_SCLK in the C5 clock tree and using the selected LEDC source frequency for timer calculations.

What Changed

  • metadata:
    • enabled LEDC for ESP32-C5
    • added LEDC_SCLK mux (PLL_F80M / RC_FAST_CLK / XTAL_CLK)
  • C5 clock implementation:
    • added enable_ledc_sclk_impl and configure_ledc_sclk_impl
    • set C5 default presets to LedcSclkConfig::PllF80m
  • LEDC HAL/timer:
    • route low-speed global LEDC clock setup through clock-tree LEDC source config when available
    • use ledc_sclk_frequency() for divider calculations when LEDC_SCLK exists (with APB fallback)
  • HIL:
    • added LEDC HIL binary (hil-test/src/bin/ledc.rs)
    • added loopback duty-cycle tests and output-frequency period test
    • registered ledc binary in hil-test/Cargo.toml
    • included esp32c5 in LEDC HIL chip list

Verification

  • cargo xtask run tests esp32c5 --test ledc --toolchain nightly --repeat 5
  • cargo xtask run tests esp32s3 --test ledc --toolchain esp --repeat 5
  • cargo xtask run tests esp32c5 --test rmt --toolchain nightly (regression)
  • build sanity for LEDC HIL:
    • cargo +nightly build --manifest-path hil-test/Cargo.toml --config hil-test/.cargo/config.toml --target=riscv32imc-unknown-none-elf --features=unstable,esp32c3 --bin=ledc --release
    • cargo +nightly build --manifest-path hil-test/Cargo.toml --config hil-test/.cargo/config.toml --target=riscv32imac-unknown-none-elf --features=unstable,esp32c6 --bin=ledc --release
    • cargo +nightly build --manifest-path hil-test/Cargo.toml --config hil-test/.cargo/config.toml --target=riscv32imac-unknown-none-elf --features=unstable,esp32h2 --bin=ledc --release
  • workflow path spot-check:
    • cargo xtask build tests esp32c5 --test ledc --toolchain nightly
    • cargo xtask run elfs esp32c5 target/tests/esp32c5 --elfs ledc
    • cargo xtask build tests esp32s3 --test ledc --toolchain esp
    • cargo xtask run elfs esp32s3 target/tests/esp32s3 --elfs ledc

Notes

  • no intended public API changes

Copilot AI review requested due to automatic review settings February 11, 2026 20:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds ESP32-C5 support for the LEDC peripheral by wiring it into the device metadata/clock tree, updating the LEDC HAL to use the correct timer source frequency for divider calculations, and introducing a new HIL loopback test binary to validate duty-cycle and output-frequency behavior across chips.

Changes:

  • Enable LEDC for ESP32-C5 in metadata and model the LEDC_SCLK clock mux (PLL_F80M / RC_FAST_CLK / XTAL_CLK).
  • Update LEDC timer divider calculations to use ledc_sclk_frequency() when the clock-tree node exists (APB fallback otherwise).
  • Add hil-test LEDC loopback tests and include esp32c5 in the LEDC HIL chip matrix.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
hil-test/src/bin/ledc.rs Adds LEDC HIL loopback tests for duty-cycle changes, 0% duty behavior, and output period validation.
hil-test/Cargo.toml Registers the new ledc HIL binary.
esp-metadata/devices/esp32c5.toml Enables the LEDC peripheral and adds the LEDC_SCLK clock-tree mux node for ESP32-C5.
esp-metadata-generated/src/_generated_esp32c5.rs Regenerates ESP32-C5 clock tree types/APIs to include LEDC_SCLK and LEDC peripheral controls.
esp-metadata-generated/src/_build_script_utils.rs Emits new cfgs for LEDC presence/support and soc_has_clock_node_ledc_sclk.
esp-hal/src/soc/esp32c5/clocks.rs Implements ESP32-C5 LEDC_SCLK enable/configure hooks and sets default preset to PllF80m.
esp-hal/src/ledc/timer.rs Uses LEDC_SCLK frequency (when available) for LS timer source frequency/divider math.
esp-hal/src/ledc/mod.rs Routes global slow LEDC clock setup through the clock-tree config when available; updates PCR handling for esp32c5.
esp-hal/src/ledc/channel.rs Adjusts channel availability for esp32c5 and adds esp32c5-specific duty/fade start handling.
esp-hal/README.md Updates the capabilities table to mark LEDC supported for esp32c5.

Comment thread esp-hal/src/ledc/mod.rs Outdated
Comment on lines 172 to 174
#[cfg(esp32c6)]
pcr.ledc_sclk_conf()
.write(|w| unsafe { w.ledc_sclk_sel().bits(1) });
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On esp32c6 this ledc_sclk_conf().write(|w| w.ledc_sclk_sel().bits(1)) call clears ledc_sclk_en (because write starts from the reset value). Since ledc_sclk_en was set just above, this effectively disables LEDC_SCLK again. Use modify here (or set both ledc_sclk_en and ledc_sclk_sel in the same write) so the enable bit stays asserted.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 50b9fe5e: on esp32c6/esp32h2 we now use modify and set ledc_sclk_sel while keeping ledc_sclk_en asserted, so the clock gate is not accidentally cleared by a later write.

Comment thread esp-hal/src/ledc/channel.rs Outdated
Comment on lines +515 to +519
let _ = (duty_inc, duty_steps, cycles_per_step, duty_per_cycle);
self.ledc
.ch(self.number as usize)
.conf1()
.write(|w| w.duty_start().set_bit());
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The esp32c5 start_duty_fade_inner implementation ignores all fade parameters and only sets duty_start. As a result, start_duty_fade() can return Ok(()) but not perform a fade (and is_duty_fade_running() may not reflect reality). Either implement the actual fade configuration for ESP32-C5 or make start_duty_fade return an error/unsupported on esp32c5.

Suggested change
let _ = (duty_inc, duty_steps, cycles_per_step, duty_per_cycle);
self.ledc
.ch(self.number as usize)
.conf1()
.write(|w| w.duty_start().set_bit());
let cnum = self.number as usize;
self.ledc
.ch(cnum)
.conf1()
.write(|w| w.duty_start().set_bit());
self.ledc.ch_gamma_wr(cnum).write(|w| unsafe {
w.ch_gamma_duty_inc()
.variant(duty_inc)
.ch_gamma_duty_num() // count of incs before stopping
.bits(duty_steps)
.ch_gamma_duty_cycle() // overflows between incs
.bits(cycles_per_step)
.ch_gamma_scale()
.bits(duty_per_cycle)
});
self.ledc
.ch_gamma_wr_addr(cnum)
.write(|w| unsafe { w.ch_gamma_wr_addr().bits(0) });
self.ledc
.ch_gamma_conf(cnum)
.write(|w| unsafe { w.ch_gamma_entry_num().bits(0x1) });

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented for ESP32-C5 in the latest commit (846d86e5).

What changed:

  • Added real fade parameter programming for C5 using the documented gamma RAM window (LEDC + 0x400, 16 ranges/channel), since the current C5 PAC does not expose ch_gamma_wr* registers.
  • Set up fade-complete signaling on C5 (int_clr + int_ena) and made is_duty_fade_running() read int_st for C5.
  • Fixed the C5 init path by clearing PCR.LEDC_PD_CTRL.LEDC_MEM_FORCE_PD in Ledc::new(); this bit resets to force power-down and otherwise gamma RAM writes do not take effect.
  • Added a C5 HIL test (duty_fade_changes_output_and_completes) that verifies fade starts, completes, and actually changes waveform duty.

Verification run on hardware (esp32c5): 4/4 tests passing, repeated multiple times.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: I opened esp-rs/esp-pacs#397 to expose C5 gamma RAM in PAC as a typed LEDC_GAMMA_RAM peripheral (0x6000_7400, 96 entries = 16 ranges x 6 channels).

Small wording correction to my previous note: on C5 the missing piece is gamma RAM range register exposure (not ch_gamma_wr*, which is the C6/H2 register model).

@Veanir Veanir marked this pull request as draft February 11, 2026 20:59
Clear forced LEDC memory power-down on C5 and program gamma RAM ranges so fade parameters take effect. Add a C5 HIL fade test to verify duty progression and completion on hardware.
@Veanir
Copy link
Copy Markdown
Contributor Author

Veanir commented Feb 11, 2026

Quick status update: I tested a local esp-hal refactor that uses the new typed C5 gamma RAM PAC API from esp-rs/esp-pacs#397 (instead of raw MMIO), and it passes C5 HIL (hil-test/ledc) repeatedly on hardware.

I did not push that dependency switch here, so this PR stays on upstream PAC. Once esp-pacs#397 is merged, I can follow up immediately with the PAC-API cleanup commit in this PR branch.

Comment thread hil-test/src/bin/ledc.rs
Comment on lines +35 to +41
for _ in 0..SAMPLE_COUNT {
if input.is_high() {
high_count += 1;
}

delay.delay_micros(SAMPLE_INTERVAL_US);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you count transitions instead? This is a bit too complex with the underlying timer and all, and I'm not sure that your 37us sampling interval is actually 37us. You could just loop a GPIO read instead, and bump the counter when it changes from low to high.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 41259374: removed delay-based GPIO sampling. sample_high_count is now a tight GPIO read loop (no assumed microsecond delay), and the frequency test measures period by counting rising-edge transitions directly. Verified on ESP32-C5 HIL (3 runs, 4/4 tests passing).

Comment thread hil-test/src/bin/ledc.rs Outdated
return true;
}

delay.delay_micros(EDGE_POLL_INTERVAL_US);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wait? Just loop, and if the transition doesn't happen, the test will time out anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 41259374: removed the explicit wait helper + manual iteration limits; edge detection now just polls in a tight loop and relies on the embedded-test timeout if the signal does not toggle.

On esp32c6/esp32h2, selecting LEDC_SCLK with a  cleared  back to reset, effectively disabling the clock gate again. Use  and set both the mux selection and enable bit together.
Remove delay-based GPIO sampling and edge-wait helpers; count levels in a tight loop and measure PWM period by counting rising edges. This avoids assuming microsecond delays are precise and relies on the embedded-test timeout if edges are missing.
# ## Miscellaneous
# [device.io_mux]
[device.systimer]
[device.ledc]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a has_gamma_fade cfg to this, please

Comment thread esp-hal/src/ledc/mod.rs Outdated
});

#[cfg(any(esp32c5, esp32c6, esp32h2))]
let pcr = unsafe { &*crate::peripherals::PCR::ptr() };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I have to ask if this is AI generated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah (Codex 5.3 to be exact) - is it strictly forbidden here? Upon some reflection - I should've marked it somewhere.

I needed RMT and LEDC with C5 in my project - once I got it working and tested it locally I thought I could contribute here.

If I'm not being usefull here (or even mallicious) just let me know :)

Copy link
Copy Markdown
Contributor

@bugadani bugadani Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well we ask people to disclose the use of AI, but my main observation is that this is outdated code (use PCR::regs(), and generally not up to the latest standards we have (which is gating it with metadata-generated symbols, like soc_has_pcr in this case).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - I’ll review the current project conventions

@Veanir Veanir force-pushed the fix/esp32c5-ledc-hil branch from 25104ff to d82e5f7 Compare February 16, 2026 17:26
@github-actions github-actions Bot added the merge-conflict Merge conflict detected. Automatically added/removed by CI. label Feb 17, 2026
@github-actions
Copy link
Copy Markdown

New commits in main has made this PR unmergable. Please resolve the conflicts.

@wallem89
Copy link
Copy Markdown

@Veanir will you be continue working on this PR anytime soon? I need RMT and LEDC with C5 in my project too and I am already struggling to get this project to work on the current main. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-conflict Merge conflict detected. Automatically added/removed by CI.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants