Skip to content
2 changes: 1 addition & 1 deletion esp-hal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ For help getting started with this HAL, please refer to [The Rust on ESP Book] a
| IOMUX | ⚒️ | ⚒️ | ⚒️ | | ⚒️ | ⚒️ | ⚒️ | ⚒️ |
| Camera interface | ❌ | | | | | | ❌ | ⚒️ |
| RGB display | ⚒️ | | | | | | ❌ | ⚒️ |
| LEDC | ⚒️ | ⚒️ | ⚒️ | | ⚒️ | ⚒️ | ⚒️ | ⚒️ |
| LEDC | ⚒️ | ⚒️ | ⚒️ | ⚒️ | ⚒️ | ⚒️ | ⚒️ | ⚒️ |
| MCPWM | ⚒️ | | | | ⚒️ | ⚒️ | | ⚒️ |
| PARL_IO | | | | | ⚒️ | ⚒️ | | |
| PCNT | ⚒️ | | | ⚒️ | ⚒️ | ⚒️ | ⚒️ | ⚒️ |
Expand Down
128 changes: 114 additions & 14 deletions esp-hal/src/ledc/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
use super::timer::{TimerIFace, TimerSpeed};
use crate::{
gpio::{
DriveMode,
OutputConfig,
OutputSignal,
DriveMode, OutputConfig, OutputSignal,
interconnect::{self, PeripheralOutput},
},
pac::ledc::RegisterBlock,
Expand Down Expand Up @@ -65,10 +63,10 @@ pub enum Number {
Channel4 = 4,
/// Channel 5
Channel5 = 5,
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
#[cfg(not(any(esp32c2, esp32c3, esp32c5, esp32c6, esp32h2)))]
/// Channel 6
Channel6 = 6,
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
#[cfg(not(any(esp32c2, esp32c3, esp32c5, esp32c6, esp32h2)))]
/// Channel 7
Channel7 = 7,
}
Expand Down Expand Up @@ -335,6 +333,40 @@ mod ehal1 {
}

impl<S: crate::ledc::timer::TimerSpeed> Channel<'_, S> {
#[cfg(esp32c5)]
const C5_GAMMA_RAM_BASE_OFFSET: usize = 0x400;
#[cfg(esp32c5)]
const C5_GAMMA_RAM_CHANNEL_STRIDE: usize = 0x40;
#[cfg(esp32c5)]
const C5_GAMMA_RANGE_COUNT: usize = 16;

#[cfg(esp32c5)]
fn c5_enable_gamma_ram_clock(&self) {
self.ledc.conf().modify(|_, w| match self.number {
Number::Channel0 => w.gamma_ram_clk_en_ch0().set_bit(),
Number::Channel1 => w.gamma_ram_clk_en_ch1().set_bit(),
Number::Channel2 => w.gamma_ram_clk_en_ch2().set_bit(),
Number::Channel3 => w.gamma_ram_clk_en_ch3().set_bit(),
Number::Channel4 => w.gamma_ram_clk_en_ch4().set_bit(),
Number::Channel5 => w.gamma_ram_clk_en_ch5().set_bit(),
});
}

#[cfg(esp32c5)]
fn c5_write_gamma_range(&self, range: usize, value: u32) {
let offset = Self::C5_GAMMA_RAM_BASE_OFFSET
+ (self.number as usize) * Self::C5_GAMMA_RAM_CHANNEL_STRIDE
+ range * core::mem::size_of::<u32>();

let reg = (LEDC::PTR as *mut u8).wrapping_add(offset).cast::<u32>();

unsafe {
// SAFETY: `reg` is computed from the LEDC peripheral base and points to one
// of the documented gamma RAM entries for this channel/range.
reg.write_volatile(value);
}
}

#[cfg(esp32)]
fn set_channel(&mut self, timer_number: u8) {
if S::IS_HS {
Expand Down Expand Up @@ -362,7 +394,7 @@ impl<S: crate::ledc::timer::TimerSpeed> Channel<'_, S> {
}

// this is needed to make low duty-resolutions / high frequencies work
#[cfg(any(esp32h2, esp32c6))]
#[cfg(any(esp32c6, esp32h2))]
self.ledc
.ch_gamma_wr_addr(self.number as usize)
.write(|w| unsafe { w.bits(0) });
Expand Down Expand Up @@ -412,7 +444,14 @@ impl<S: crate::ledc::timer::TimerSpeed> Channel<'_, S> {
}
});
}
#[cfg(not(any(esp32, esp32c6, esp32h2)))]
#[cfg(esp32c5)]
fn start_duty_without_fading(&self) {
self.ledc
.ch(self.number as usize)
.conf1()
.write(|w| w.duty_start().set_bit());
}
#[cfg(not(any(esp32, esp32c5, esp32c6, esp32h2)))]
fn start_duty_without_fading(&self) {
self.ledc.ch(self.number as usize).conf1().write(|w| {
w.duty_start().set_bit();
Expand Down Expand Up @@ -499,7 +538,35 @@ impl<S: crate::ledc::timer::TimerSpeed> Channel<'_, S> {
.write(|w| unsafe { w.ch_gamma_entry_num().bits(0x1) });
}

#[cfg(not(any(esp32, esp32c6, esp32h2)))]
#[cfg(esp32c5)]
fn start_duty_fade_inner(
&self,
duty_inc: bool,
duty_steps: u16,
cycles_per_step: u16,
duty_per_cycle: u16,
) {
let cnum = self.number as usize;
let range_cfg = u32::from(duty_inc)
| (u32::from(cycles_per_step) << 1)
| (u32::from(duty_per_cycle) << 11)
| (u32::from(duty_steps) << 21);

self.c5_enable_gamma_ram_clock();
self.c5_write_gamma_range(0, range_cfg);
for range in 1..Self::C5_GAMMA_RANGE_COUNT {
self.c5_write_gamma_range(range, 0);
}
self.ledc
.ch_gamma_conf(cnum)
.write(|w| unsafe { w.ch_gamma_entry_num().bits(0x1) });
self.ledc
.ch(cnum)
.conf1()
.write(|w| w.duty_start().set_bit());
}

#[cfg(not(any(esp32, esp32c5, esp32c6, esp32h2)))]
fn start_duty_fade_inner(
&self,
duty_inc: bool,
Expand Down Expand Up @@ -583,9 +650,9 @@ where
Number::Channel3 => OutputSignal::LEDC_LS_SIG3,
Number::Channel4 => OutputSignal::LEDC_LS_SIG4,
Number::Channel5 => OutputSignal::LEDC_LS_SIG5,
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
#[cfg(not(any(esp32c2, esp32c3, esp32c5, esp32c6, esp32h2)))]
Number::Channel6 => OutputSignal::LEDC_LS_SIG6,
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
#[cfg(not(any(esp32c2, esp32c3, esp32c5, esp32c6, esp32h2)))]
Number::Channel7 => OutputSignal::LEDC_LS_SIG7,
}
};
Expand All @@ -597,9 +664,9 @@ where
Number::Channel3 => OutputSignal::LEDC_LS_SIG3,
Number::Channel4 => OutputSignal::LEDC_LS_SIG4,
Number::Channel5 => OutputSignal::LEDC_LS_SIG5,
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
#[cfg(not(any(esp32c2, esp32c3, esp32c5, esp32c6, esp32h2)))]
Number::Channel6 => OutputSignal::LEDC_LS_SIG6,
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
#[cfg(not(any(esp32c2, esp32c3, esp32c5, esp32c6, esp32h2)))]
Number::Channel7 => OutputSignal::LEDC_LS_SIG7,
};

Expand Down Expand Up @@ -672,7 +739,31 @@ where
}

/// Start a duty-cycle fade HW
#[cfg(not(esp32))]
#[cfg(esp32c5)]
fn start_duty_fade_hw(
&self,
start_duty: u32,
duty_inc: bool,
duty_steps: u16,
cycles_per_step: u16,
duty_per_cycle: u16,
) {
self.ledc
.ch(self.number as usize)
.duty()
.write(|w| unsafe { w.duty().bits(start_duty << 4) });
self.ledc
.int_clr()
.write(|w| w.duty_chng_end_ch(self.number as u8).clear_bit_by_one());
self.ledc
.int_ena()
.modify(|_, w| w.duty_chng_end_ch(self.number as u8).set_bit());
self.start_duty_fade_inner(duty_inc, duty_steps, cycles_per_step, duty_per_cycle);
self.update_channel();
}

/// Start a duty-cycle fade HW
#[cfg(not(any(esp32, esp32c5)))]
fn start_duty_fade_hw(
&self,
start_duty: u32,
Expand Down Expand Up @@ -702,7 +793,16 @@ where
}
}

#[cfg(not(esp32))]
#[cfg(esp32c5)]
fn is_duty_fade_running_hw(&self) -> bool {
self.ledc
.int_st()
.read()
.duty_chng_end_ch(self.number as u8)
.bit_is_clear()
}

#[cfg(not(any(esp32, esp32c5)))]
fn is_duty_fade_running_hw(&self) -> bool {
self.ledc
.int_raw()
Expand Down
43 changes: 34 additions & 9 deletions esp-hal/src/ledc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ impl<'d> Ledc<'d> {
PeripheralClockControl::reset(PeripheralEnable::Ledc);
}

#[cfg(esp32c5)]
unsafe {
// SAFETY: writing PCR LEDC power control is required to unforce power-down
// of the LEDC memory block, which backs gamma fade parameters.
(&*crate::peripherals::PCR::ptr())
.ledc_pd_ctrl()
.modify(|_, w| w.ledc_mem_force_pd().clear_bit());
}

let ledc = LEDC::regs();
Ledc { _instance, ledc }
}
Expand All @@ -142,24 +151,40 @@ impl<'d> Ledc<'d> {
#[cfg(not(esp32))]
/// Set global slow clock source
pub fn set_global_slow_clock(&mut self, clock_source: LSGlobalClkSource) {
#[cfg(any(esp32c6, esp32h2))]
let pcr = unsafe { &*crate::peripherals::PCR::ptr() };
#[cfg(soc_has_clock_node_ledc_sclk)]
let ledc_sclk = match clock_source {
LSGlobalClkSource::APBClk => crate::soc::clocks::LedcSclkConfig::PllF80m,
};

#[cfg(any(esp32c6, esp32h2))]
pcr.ledc_sclk_conf().write(|w| w.ledc_sclk_en().set_bit());
#[cfg(soc_has_clock_node_ledc_sclk)]
crate::soc::clocks::ClockTree::with(|clocks| {
crate::soc::clocks::configure_ledc_sclk(clocks, ledc_sclk);
});

#[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 :)

@bugadani bugadani Feb 12, 2026

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.

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


match clock_source {
LSGlobalClkSource::APBClk => {
#[cfg(not(any(esp32c6, esp32h2)))]
#[cfg(not(any(esp32c5, esp32c6, esp32h2)))]
self.ledc
.conf()
.write(|w| unsafe { w.apb_clk_sel().bits(1) });
#[cfg(esp32c5)]
pcr.ledc_sclk_conf().modify(|_, w| unsafe {
w.ledc_sclk_sel().bits(2);
w.ledc_sclk_en().set_bit()
});
#[cfg(esp32c6)]
pcr.ledc_sclk_conf()
.write(|w| unsafe { w.ledc_sclk_sel().bits(1) });
pcr.ledc_sclk_conf().modify(|_, w| unsafe {
w.ledc_sclk_sel().bits(1);
w.ledc_sclk_en().set_bit()
});
#[cfg(esp32h2)]
pcr.ledc_sclk_conf()
.write(|w| unsafe { w.ledc_sclk_sel().bits(0) });
pcr.ledc_sclk_conf().modify(|_, w| unsafe {
w.ledc_sclk_sel().bits(0);
w.ledc_sclk_en().set_bit()
});
}
}
self.ledc
Expand Down
24 changes: 20 additions & 4 deletions esp-hal/src/ledc/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
//! duty cycles and frequencies, making it ideal for Pulse-Width Modulation
//! (PWM) applications and LED lighting control.
//!
//! LEDC uses APB as clock source.
//! LEDC timer source clock is chip-specific.

#[cfg(esp32)]
use super::HighSpeed;
use super::{LowSpeed, Speed};
use crate::{clock::Clocks, pac, time::Rate};
use crate::clock::Clocks;
#[cfg(soc_has_clock_node_ledc_sclk)]
use crate::soc::clocks::ClockTree;
use crate::{pac, time::Rate};

const LEDC_TIMER_DIV_NUM_MAX: u64 = 0x3FFFF;

Expand Down Expand Up @@ -315,8 +318,21 @@ impl TimerHW<LowSpeed> for Timer<'_, LowSpeed> {
fn freq_hw(&self) -> Option<Rate> {
self.clock_source.map(|source| match source {
LSClockSource::APBClk => {
let clocks = Clocks::get();
clocks.apb_clock
#[cfg(soc_has_clock_node_ledc_sclk)]
{
ClockTree::with(|clocks| {
if crate::soc::clocks::ledc_sclk_config(clocks).is_some() {
Rate::from_hz(crate::soc::clocks::ledc_sclk_frequency(clocks))
} else {
Clocks::get().apb_clock
}
})
}
#[cfg(not(soc_has_clock_node_ledc_sclk))]
{
let clocks = Clocks::get();
clocks.apb_clock
}
}
})
}
Expand Down
27 changes: 26 additions & 1 deletion esp-hal/src/soc/esp32c5/clocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ define_clock_tree_types!();
pub enum CpuClock {
/// 80 MHz CPU clock
#[default]
_80MHz = 80,
_80MHz = 80,

/// 160 MHz CPU clock
_160MHz = 160,
Expand All @@ -48,6 +48,7 @@ impl CpuClock {
cpu_clk: Some(CpuClkConfig(1)),
ahb_clk: Some(AhbClkConfig(3)), // 40MHz - cannot exceed XTAL_CLK
apb_clk: Some(ApbClkConfig(0)),
ledc_sclk: Some(LedcSclkConfig::PllF80m),
lp_fast_clk: Some(LpFastClkConfig::RcFast),
lp_slow_clk: Some(LpSlowClkConfig::RcSlow),
timg_calibration_clock: None,
Expand All @@ -58,6 +59,7 @@ impl CpuClock {
cpu_clk: Some(CpuClkConfig(0)),
ahb_clk: Some(AhbClkConfig(3)), // 40MHz - cannot exceed XTAL_CLK
apb_clk: Some(ApbClkConfig(0)),
ledc_sclk: Some(LedcSclkConfig::PllF80m),
lp_fast_clk: Some(LpFastClkConfig::RcFast),
lp_slow_clk: Some(LpSlowClkConfig::RcSlow),
timg_calibration_clock: None,
Expand All @@ -68,6 +70,7 @@ impl CpuClock {
cpu_clk: Some(CpuClkConfig(0)),
ahb_clk: Some(AhbClkConfig(5)), // 40MHz - cannot exceed XTAL_CLK
apb_clk: Some(ApbClkConfig(0)),
ledc_sclk: Some(LedcSclkConfig::PllF80m),
lp_fast_clk: Some(LpFastClkConfig::RcFast),
lp_slow_clk: Some(LpSlowClkConfig::RcSlow),
timg_calibration_clock: None,
Expand Down Expand Up @@ -387,6 +390,28 @@ fn configure_apb_clk_impl(_clocks: &mut ClockTree, new_config: ApbClkConfig) {
.modify(|_, w| unsafe { w.apb_div_num().bits(new_config.value() as u8) });
}

// LEDC_SCLK

fn enable_ledc_sclk_impl(_clocks: &mut ClockTree, en: bool) {
PCR::regs()
.ledc_sclk_conf()
.modify(|_, w| w.ledc_sclk_en().bit(en));
}

fn configure_ledc_sclk_impl(
_clocks: &mut ClockTree,
_old_selector: Option<LedcSclkConfig>,
new_selector: LedcSclkConfig,
) {
PCR::regs().ledc_sclk_conf().modify(|_, w| unsafe {
w.ledc_sclk_sel().bits(match new_selector {
LedcSclkConfig::XtalClk => 0,
LedcSclkConfig::RcFastClk => 1,
LedcSclkConfig::PllF80m => 2,
})
});
}

// XTAL_D2_CLK

fn enable_xtal_d2_clk_impl(_clocks: &mut ClockTree, _en: bool) {
Expand Down
Loading
Loading