feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+727
View File
@@ -0,0 +1,727 @@
//! # LEDC channel
//!
//! ## Overview
//! The LEDC Channel module provides a high-level interface to
//! configure and control individual PWM channels of the LEDC peripheral.
//!
//! ## Configuration
//! The module allows precise and flexible control over LED lighting and other
//! `Pulse-Width Modulation (PWM)` applications by offering configurable duty
//! cycles and frequencies.
use super::timer::{TimerIFace, TimerSpeed};
use crate::{
gpio::{
interconnect::{OutputConnection, PeripheralOutput},
OutputSignal,
},
peripheral::{Peripheral, PeripheralRef},
peripherals::ledc::RegisterBlock,
};
/// Fade parameter sub-errors
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum FadeError {
/// Start duty % out of range
StartDuty,
/// End duty % out of range
EndDuty,
/// Duty % change from start to end is out of range
DutyRange,
/// Duration too long for timer frequency and duty resolution
Duration,
}
/// Channel errors
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Error {
/// Invalid duty % value
Duty,
/// Timer not configured
Timer,
/// Channel not configured
Channel,
/// Fade parameters invalid
Fade(FadeError),
}
/// Channel number
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Number {
/// Channel 0
Channel0 = 0,
/// Channel 1
Channel1 = 1,
/// Channel 2
Channel2 = 2,
/// Channel 3
Channel3 = 3,
/// Channel 4
Channel4 = 4,
/// Channel 5
Channel5 = 5,
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
/// Channel 6
Channel6 = 6,
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
/// Channel 7
Channel7 = 7,
}
/// Channel configuration
pub mod config {
use crate::ledc::timer::{TimerIFace, TimerSpeed};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
/// Pin configuration for the LEDC channel.
pub enum PinConfig {
/// Push-pull pin configuration.
PushPull,
/// Open-drain pin configuration.
OpenDrain,
}
/// Channel configuration
#[derive(Copy, Clone)]
pub struct Config<'a, S: TimerSpeed> {
/// A reference to the timer associated with this channel.
pub timer: &'a dyn TimerIFace<S>,
/// The duty cycle percentage (0-100).
pub duty_pct: u8,
/// The pin configuration (PushPull or OpenDrain).
pub pin_config: PinConfig,
}
}
/// Channel interface
pub trait ChannelIFace<'a, S: TimerSpeed + 'a>
where
Channel<'a, S>: ChannelHW,
{
/// Configure channel
fn configure(&mut self, config: config::Config<'a, S>) -> Result<(), Error>;
/// Set channel duty HW
fn set_duty(&self, duty_pct: u8) -> Result<(), Error>;
/// Start a duty-cycle fade
fn start_duty_fade(
&self,
start_duty_pct: u8,
end_duty_pct: u8,
duration_ms: u16,
) -> Result<(), Error>;
/// Check whether a duty-cycle fade is running
fn is_duty_fade_running(&self) -> bool;
}
/// Channel HW interface
pub trait ChannelHW {
/// Configure Channel HW except for the duty which is set via
/// [`Self::set_duty_hw`].
fn configure_hw(&mut self) -> Result<(), Error>;
/// Configure the hardware for the channel with a specific pin
/// configuration.
fn configure_hw_with_pin_config(&mut self, cfg: config::PinConfig) -> Result<(), Error>;
/// Set channel duty HW
fn set_duty_hw(&self, duty: u32);
/// Start a duty-cycle fade HW
fn start_duty_fade_hw(
&self,
start_duty: u32,
duty_inc: bool,
duty_steps: u16,
cycles_per_step: u16,
duty_per_cycle: u16,
);
/// Check whether a duty-cycle fade is running HW
fn is_duty_fade_running_hw(&self) -> bool;
}
/// Channel struct
pub struct Channel<'a, S: TimerSpeed> {
ledc: &'a RegisterBlock,
timer: Option<&'a dyn TimerIFace<S>>,
number: Number,
output_pin: PeripheralRef<'a, OutputConnection>,
}
impl<'a, S: TimerSpeed> Channel<'a, S> {
/// Return a new channel
pub fn new(
number: Number,
output_pin: impl Peripheral<P = impl PeripheralOutput> + 'a,
) -> Self {
crate::into_mapped_ref!(output_pin);
let ledc = unsafe { &*crate::peripherals::LEDC::ptr() };
Channel {
ledc,
timer: None,
number,
output_pin,
}
}
}
impl<'a, S: TimerSpeed> ChannelIFace<'a, S> for Channel<'a, S>
where
Channel<'a, S>: ChannelHW,
{
/// Configure channel
fn configure(&mut self, config: config::Config<'a, S>) -> Result<(), Error> {
self.timer = Some(config.timer);
self.set_duty(config.duty_pct)?;
self.configure_hw_with_pin_config(config.pin_config)?;
Ok(())
}
/// Set duty % of channel
fn set_duty(&self, duty_pct: u8) -> Result<(), Error> {
let duty_exp;
if let Some(timer) = self.timer {
if let Some(timer_duty) = timer.duty() {
duty_exp = timer_duty as u32;
} else {
return Err(Error::Timer);
}
} else {
return Err(Error::Channel);
}
let duty_range = 2u32.pow(duty_exp);
let duty_value = (duty_range * duty_pct as u32) / 100;
if duty_pct > 100u8 {
// duty_pct greater than 100%
return Err(Error::Duty);
}
self.set_duty_hw(duty_value);
Ok(())
}
/// Start a duty fade from one % to another.
///
/// There's a constraint on the combination of timer frequency, timer PWM
/// duty resolution (the bit count), the fade "range" (abs(start-end)), and
/// the duration:
///
/// frequency * duration / ((1<<bit_count) * abs(start-end)) < 1024
///
/// Small percentage changes, long durations, coarse PWM resolutions (that
/// is, low bit counts), and high timer frequencies will all be more likely
/// to fail this requirement. If it does fail, this function will return
/// an error Result.
fn start_duty_fade(
&self,
start_duty_pct: u8,
end_duty_pct: u8,
duration_ms: u16,
) -> Result<(), Error> {
let duty_exp;
let frequency;
if start_duty_pct > 100u8 {
return Err(Error::Fade(FadeError::StartDuty));
}
if end_duty_pct > 100u8 {
return Err(Error::Fade(FadeError::EndDuty));
}
if let Some(timer) = self.timer {
if let Some(timer_duty) = timer.duty() {
if timer.frequency() > 0 {
duty_exp = timer_duty as u32;
frequency = timer.frequency();
} else {
return Err(Error::Timer);
}
} else {
return Err(Error::Timer);
}
} else {
return Err(Error::Channel);
}
let duty_range = (1u32 << duty_exp) - 1;
let start_duty_value = (duty_range * start_duty_pct as u32) / 100;
let end_duty_value = (duty_range * end_duty_pct as u32) / 100;
// NB: since we do the multiplication first here, there's no loss of
// precision from using milliseconds instead of (e.g.) nanoseconds.
let pwm_cycles = (duration_ms as u32) * frequency / 1000;
let abs_duty_diff = end_duty_value.abs_diff(start_duty_value);
let duty_steps: u32 = u16::try_from(abs_duty_diff).unwrap_or(65535).into();
// This conversion may fail if duration_ms is too big, and if either
// duty_steps gets truncated, or the fade is over a short range of duty
// percentages, so it's too small. Returning an Err in either case is
// fine: shortening the duration_ms will sort things out.
let cycles_per_step: u16 = (pwm_cycles / duty_steps)
.try_into()
.map_err(|_| Error::Fade(FadeError::Duration))
.and_then(|res| {
if res > 1023 {
Err(Error::Fade(FadeError::Duration))
} else {
Ok(res)
}
})?;
// This can't fail unless abs_duty_diff is bigger than 65536*65535-1,
// and so duty_steps gets truncated. But that requires duty_exp to be
// at least 32, and the hardware only supports up to 20. Still, handle
// it in case something changes in the future.
let duty_per_cycle: u16 = (abs_duty_diff / duty_steps)
.try_into()
.map_err(|_| Error::Fade(FadeError::DutyRange))?;
self.start_duty_fade_hw(
start_duty_value,
end_duty_value > start_duty_value,
duty_steps.try_into().unwrap(),
cycles_per_step,
duty_per_cycle,
);
Ok(())
}
fn is_duty_fade_running(&self) -> bool {
self.is_duty_fade_running_hw()
}
}
mod ehal1 {
use embedded_hal::pwm::{self, ErrorKind, ErrorType, SetDutyCycle};
use super::{Channel, ChannelHW, Error};
use crate::ledc::timer::TimerSpeed;
impl pwm::Error for Error {
fn kind(&self) -> pwm::ErrorKind {
ErrorKind::Other
}
}
impl<S: TimerSpeed> ErrorType for Channel<'_, S> {
type Error = Error;
}
impl<'a, S: TimerSpeed> SetDutyCycle for Channel<'a, S>
where
Channel<'a, S>: ChannelHW,
{
fn max_duty_cycle(&self) -> u16 {
let duty_exp;
if let Some(timer_duty) = self.timer.and_then(|timer| timer.duty()) {
duty_exp = timer_duty as u32;
} else {
return 0;
}
let duty_range = 2u32.pow(duty_exp);
duty_range as u16
}
fn set_duty_cycle(&mut self, mut duty: u16) -> Result<(), Self::Error> {
let max = self.max_duty_cycle();
duty = if duty > max { max } else { duty };
self.set_duty_hw(duty.into());
Ok(())
}
}
}
impl<S: crate::ledc::timer::TimerSpeed> Channel<'_, S> {
#[cfg(esp32)]
fn set_channel(&mut self, timer_number: u8) {
if S::IS_HS {
let ch = self.ledc.hsch(self.number as usize);
ch.hpoint().write(|w| unsafe { w.hpoint().bits(0x0) });
ch.conf0()
.modify(|_, w| unsafe { w.sig_out_en().set_bit().timer_sel().bits(timer_number) });
} else {
let ch = self.ledc.lsch(self.number as usize);
ch.hpoint().write(|w| unsafe { w.hpoint().bits(0x0) });
ch.conf0()
.modify(|_, w| unsafe { w.sig_out_en().set_bit().timer_sel().bits(timer_number) });
}
self.start_duty_without_fading();
}
#[cfg(not(esp32))]
fn set_channel(&mut self, timer_number: u8) {
{
let ch = self.ledc.ch(self.number as usize);
ch.hpoint().write(|w| unsafe { w.hpoint().bits(0x0) });
ch.conf0().modify(|_, w| {
w.sig_out_en().set_bit();
unsafe { w.timer_sel().bits(timer_number) }
});
}
// this is needed to make low duty-resolutions / high frequencies work
#[cfg(any(esp32h2, esp32c6))]
self.ledc
.ch_gamma_wr_addr(self.number as usize)
.write(|w| unsafe { w.bits(0) });
self.start_duty_without_fading();
}
#[cfg(esp32)]
fn start_duty_without_fading(&self) {
if S::IS_HS {
self.ledc
.hsch(self.number as usize)
.conf1()
.write(|w| unsafe {
w.duty_start().set_bit();
w.duty_inc().set_bit();
w.duty_num().bits(0x1);
w.duty_cycle().bits(0x1);
w.duty_scale().bits(0x0)
});
} else {
self.ledc
.lsch(self.number as usize)
.conf1()
.write(|w| unsafe {
w.duty_start().set_bit();
w.duty_inc().set_bit();
w.duty_num().bits(0x1);
w.duty_cycle().bits(0x1);
w.duty_scale().bits(0x0)
});
}
}
#[cfg(any(esp32c6, esp32h2))]
fn start_duty_without_fading(&self) {
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| {
w.ch_gamma_duty_inc().set_bit();
unsafe {
w.ch_gamma_duty_num().bits(0x1);
w.ch_gamma_duty_cycle().bits(0x1);
w.ch_gamma_scale().bits(0x0)
}
});
}
#[cfg(not(any(esp32, esp32c6, esp32h2)))]
fn start_duty_without_fading(&self) {
self.ledc.ch(self.number as usize).conf1().write(|w| {
w.duty_start().set_bit();
w.duty_inc().set_bit();
unsafe {
w.duty_num().bits(0x1);
w.duty_cycle().bits(0x1);
w.duty_scale().bits(0x0)
}
});
}
#[cfg(esp32)]
fn start_duty_fade_inner(
&self,
duty_inc: bool,
duty_steps: u16,
cycles_per_step: u16,
duty_per_cycle: u16,
) {
if S::IS_HS {
self.ledc
.hsch(self.number as usize)
.conf1()
.write(|w| unsafe {
w.duty_start()
.set_bit()
.duty_inc()
.variant(duty_inc)
.duty_num() // count of incs before stopping
.bits(duty_steps)
.duty_cycle() // overflows between incs
.bits(cycles_per_step)
.duty_scale()
.bits(duty_per_cycle)
});
} else {
self.ledc
.lsch(self.number as usize)
.conf1()
.write(|w| unsafe {
w.duty_start()
.set_bit()
.duty_inc()
.variant(duty_inc)
.duty_num() // count of incs before stopping
.bits(duty_steps)
.duty_cycle() // overflows between incs
.bits(cycles_per_step)
.duty_scale()
.bits(duty_per_cycle)
});
}
}
#[cfg(any(esp32c6, esp32h2))]
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;
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) });
}
#[cfg(not(any(esp32, esp32c6, esp32h2)))]
fn start_duty_fade_inner(
&self,
duty_inc: bool,
duty_steps: u16,
cycles_per_step: u16,
duty_per_cycle: u16,
) {
self.ledc
.ch(self.number as usize)
.conf1()
.write(|w| unsafe {
w.duty_start().set_bit();
w.duty_inc().variant(duty_inc);
// count of incs before stopping
w.duty_num().bits(duty_steps);
// overflows between incs
w.duty_cycle().bits(cycles_per_step);
w.duty_scale().bits(duty_per_cycle)
});
}
#[cfg(esp32)]
fn update_channel(&self) {
if !S::IS_HS {
self.ledc
.lsch(self.number as usize)
.conf0()
.modify(|_, w| w.para_up().set_bit());
}
}
#[cfg(not(esp32))]
fn update_channel(&self) {
self.ledc
.ch(self.number as usize)
.conf0()
.modify(|_, w| w.para_up().set_bit());
}
}
impl<S> ChannelHW for Channel<'_, S>
where
S: crate::ledc::timer::TimerSpeed,
{
/// Configure Channel HW
fn configure_hw(&mut self) -> Result<(), Error> {
self.configure_hw_with_pin_config(config::PinConfig::PushPull)
}
fn configure_hw_with_pin_config(&mut self, cfg: config::PinConfig) -> Result<(), Error> {
if let Some(timer) = self.timer {
if !timer.is_configured() {
return Err(Error::Timer);
}
match cfg {
config::PinConfig::PushPull => self
.output_pin
.set_to_push_pull_output(crate::private::Internal),
config::PinConfig::OpenDrain => self
.output_pin
.set_to_open_drain_output(crate::private::Internal),
};
let timer_number = timer.number() as u8;
self.set_channel(timer_number);
self.update_channel();
#[cfg(esp32)]
let signal = if S::IS_HS {
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
match self.number {
Number::Channel0 => OutputSignal::LEDC_HS_SIG0,
Number::Channel1 => OutputSignal::LEDC_HS_SIG1,
Number::Channel2 => OutputSignal::LEDC_HS_SIG2,
Number::Channel3 => OutputSignal::LEDC_HS_SIG3,
Number::Channel4 => OutputSignal::LEDC_HS_SIG4,
Number::Channel5 => OutputSignal::LEDC_HS_SIG5,
Number::Channel6 => OutputSignal::LEDC_HS_SIG6,
Number::Channel7 => OutputSignal::LEDC_HS_SIG7,
}
} else {
match self.number {
Number::Channel0 => OutputSignal::LEDC_LS_SIG0,
Number::Channel1 => OutputSignal::LEDC_LS_SIG1,
Number::Channel2 => OutputSignal::LEDC_LS_SIG2,
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)))]
Number::Channel6 => OutputSignal::LEDC_LS_SIG6,
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
Number::Channel7 => OutputSignal::LEDC_LS_SIG7,
}
};
#[cfg(not(esp32))]
let signal = match self.number {
Number::Channel0 => OutputSignal::LEDC_LS_SIG0,
Number::Channel1 => OutputSignal::LEDC_LS_SIG1,
Number::Channel2 => OutputSignal::LEDC_LS_SIG2,
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)))]
Number::Channel6 => OutputSignal::LEDC_LS_SIG6,
#[cfg(not(any(esp32c2, esp32c3, esp32c6, esp32h2)))]
Number::Channel7 => OutputSignal::LEDC_LS_SIG7,
};
signal.connect_to(&mut self.output_pin);
} else {
return Err(Error::Timer);
}
Ok(())
}
/// Set duty in channel HW
#[cfg(esp32)]
fn set_duty_hw(&self, duty: u32) {
if S::IS_HS {
self.ledc
.hsch(self.number as usize)
.duty()
.write(|w| unsafe { w.duty().bits(duty << 4) });
} else {
self.ledc
.lsch(self.number as usize)
.duty()
.write(|w| unsafe { w.duty().bits(duty << 4) });
}
self.start_duty_without_fading();
self.update_channel();
}
/// Set duty in channel HW
#[cfg(not(esp32))]
fn set_duty_hw(&self, duty: u32) {
self.ledc
.ch(self.number as usize)
.duty()
.write(|w| unsafe { w.duty().bits(duty << 4) });
self.start_duty_without_fading();
self.update_channel();
}
/// Start a duty-cycle fade HW
#[cfg(esp32)]
fn start_duty_fade_hw(
&self,
start_duty: u32,
duty_inc: bool,
duty_steps: u16,
cycles_per_step: u16,
duty_per_cycle: u16,
) {
if S::IS_HS {
self.ledc
.hsch(self.number as usize)
.duty()
.write(|w| unsafe { w.duty().bits(start_duty << 4) });
self.ledc
.int_clr()
.write(|w| w.duty_chng_end_hsch(self.number as u8).clear_bit_by_one());
} else {
self.ledc
.lsch(self.number as usize)
.duty()
.write(|w| unsafe { w.duty().bits(start_duty << 4) });
self.ledc
.int_clr()
.write(|w| w.duty_chng_end_lsch(self.number as u8).clear_bit_by_one());
}
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(esp32))]
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.start_duty_fade_inner(duty_inc, duty_steps, cycles_per_step, duty_per_cycle);
self.update_channel();
}
#[cfg(esp32)]
fn is_duty_fade_running_hw(&self) -> bool {
let reg = self.ledc.int_raw().read();
if S::IS_HS {
reg.duty_chng_end_hsch(self.number as u8).bit_is_clear()
} else {
reg.duty_chng_end_lsch(self.number as u8).bit_is_clear()
}
}
#[cfg(not(esp32))]
fn is_duty_fade_running_hw(&self) -> bool {
self.ledc
.int_raw()
.read()
.duty_chng_end_ch(self.number as u8)
.bit_is_clear()
}
}
+173
View File
@@ -0,0 +1,173 @@
//! # LED Controller (LEDC)
//!
//! ## Overview
//!
//! The LEDC peripheral is primarily designed to control the intensity of LEDs,
//! although it can also be used to generate PWM signals for other purposes. It
//! has multiple channels which can generate independent waveforms that can be
//! used, for example, to drive RGB LED devices.
//!
//! The PWM controller can automatically increase or decrease the duty cycle
//! gradually, allowing for fades without any processor interference.
//!
//! ## Configuration
//! Currently only supports fixed-frequency output. High Speed channels are
//! available for the ESP32 only, while Low Speed channels are available for all
//! supported chips.
//!
//! ## Examples
//!
//! ### Low Speed Channel
//!
//! The following example will configure the Low Speed Channel0 to 24kHz output
//! with 10% duty using the ABPClock.
//!
//! ```rust, no_run
#![doc = crate::before_snippet!()]
//! # use esp_hal::ledc::Ledc;
//! # use esp_hal::ledc::LSGlobalClkSource;
//! # use esp_hal::ledc::timer;
//! # use esp_hal::ledc::LowSpeed;
//! # use esp_hal::ledc::channel;
//! # let led = peripherals.GPIO0;
//!
//! let mut ledc = Ledc::new(peripherals.LEDC);
//! ledc.set_global_slow_clock(LSGlobalClkSource::APBClk);
//!
//! let mut lstimer0 = ledc.timer::<LowSpeed>(timer::Number::Timer0);
//! lstimer0
//! .configure(timer::config::Config {
//! duty: timer::config::Duty::Duty5Bit,
//! clock_source: timer::LSClockSource::APBClk,
//! frequency: 24.kHz(),
//! })
//! .unwrap();
//!
//! let mut channel0 = ledc.channel(channel::Number::Channel0, led);
//! channel0
//! .configure(channel::config::Config {
//! timer: &lstimer0,
//! duty_pct: 10,
//! pin_config: channel::config::PinConfig::PushPull,
//! })
//! .unwrap();
//! # }
//! ```
//!
//! ## Implementation State
//! - Source clock selection is not supported
//! - Interrupts are not supported
use self::{
channel::Channel,
timer::{Timer, TimerSpeed},
};
use crate::{
gpio::interconnect::PeripheralOutput,
peripheral::{Peripheral, PeripheralRef},
system::{Peripheral as PeripheralEnable, PeripheralClockControl},
};
pub mod channel;
pub mod timer;
/// Global slow clock source
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum LSGlobalClkSource {
/// APB clock.
APBClk,
}
/// LEDC (LED PWM Controller)
pub struct Ledc<'d> {
_instance: PeripheralRef<'d, crate::peripherals::LEDC>,
ledc: &'d crate::peripherals::ledc::RegisterBlock,
}
#[cfg(esp32)]
/// Used to specify HighSpeed Timer/Channel
pub struct HighSpeed {}
/// Used to specify LowSpeed Timer/Channel
pub struct LowSpeed {}
/// Trait representing the speed mode of a clock or peripheral.
pub trait Speed {
/// Boolean constant indicating whether the speed is high-speed.
const IS_HS: bool;
}
#[cfg(esp32)]
impl Speed for HighSpeed {
const IS_HS: bool = true;
}
impl Speed for LowSpeed {
const IS_HS: bool = false;
}
impl<'d> Ledc<'d> {
/// Return a new LEDC
pub fn new(_instance: impl Peripheral<P = crate::peripherals::LEDC> + 'd) -> Self {
crate::into_ref!(_instance);
PeripheralClockControl::reset(PeripheralEnable::Ledc);
PeripheralClockControl::enable(PeripheralEnable::Ledc);
let ledc = unsafe { &*crate::peripherals::LEDC::ptr() };
Ledc { _instance, ledc }
}
/// Set global slow clock source
#[cfg(esp32)]
pub fn set_global_slow_clock(&mut self, _clock_source: LSGlobalClkSource) {
self.ledc.conf().write(|w| w.apb_clk_sel().set_bit());
self.ledc
.lstimer(0)
.conf()
.modify(|_, w| w.para_up().set_bit());
}
#[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(any(esp32c6, esp32h2))]
pcr.ledc_sclk_conf().write(|w| w.ledc_sclk_en().set_bit());
match clock_source {
LSGlobalClkSource::APBClk => {
#[cfg(not(any(esp32c6, esp32h2)))]
self.ledc
.conf()
.write(|w| unsafe { w.apb_clk_sel().bits(1) });
#[cfg(esp32c6)]
pcr.ledc_sclk_conf()
.write(|w| unsafe { w.ledc_sclk_sel().bits(1) });
#[cfg(esp32h2)]
pcr.ledc_sclk_conf()
.write(|w| unsafe { w.ledc_sclk_sel().bits(0) });
}
}
self.ledc
.timer(0)
.conf()
.modify(|_, w| w.para_up().set_bit());
}
/// Return a new timer
pub fn timer<S: TimerSpeed>(&self, number: timer::Number) -> Timer<'d, S> {
Timer::new(self.ledc, number)
}
/// Return a new channel
pub fn channel<S: TimerSpeed>(
&self,
number: channel::Number,
output_pin: impl Peripheral<P = impl PeripheralOutput> + 'd,
) -> Channel<'d, S> {
Channel::new(number, output_pin)
}
}
+404
View File
@@ -0,0 +1,404 @@
//! # LEDC timer
//!
//! ## Overview
//! The LEDC Timer provides a high-level interface to configure and control
//! individual timers of the `LEDC` peripheral.
//!
//! ## Configuration
//! The module allows precise and flexible control over timer configurations,
//! duty cycles and frequencies, making it ideal for Pulse-Width Modulation
//! (PWM) applications and LED lighting control.
//!
//! LEDC uses APB as clock source.
use fugit::HertzU32;
#[cfg(esp32)]
use super::HighSpeed;
use super::{LowSpeed, Speed};
use crate::{clock::Clocks, peripherals::ledc};
const LEDC_TIMER_DIV_NUM_MAX: u64 = 0x3FFFF;
/// Timer errors
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Error {
/// Invalid Divisor
Divisor,
}
#[cfg(esp32)]
/// Clock source for HS Timers
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum HSClockSource {
/// APB clock.
APBClk,
// TODO RefTick,
}
/// Clock source for LS Timers
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum LSClockSource {
/// APB clock.
APBClk,
// TODO SLOWClk
}
/// Timer number
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Number {
/// Timer 0.
Timer0 = 0,
/// Timer 1.
Timer1 = 1,
/// Timer 2.
Timer2 = 2,
/// Timer 3.
Timer3 = 3,
}
/// Timer configuration
pub mod config {
use fugit::HertzU32;
/// Number of bits reserved for duty cycle adjustment
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Duty {
/// 1-bit resolution for duty cycle adjustment.
Duty1Bit = 1,
/// 2-bit resolution for duty cycle adjustment.
Duty2Bit,
/// 3-bit resolution for duty cycle adjustment.
Duty3Bit,
/// 4-bit resolution for duty cycle adjustment.
Duty4Bit,
/// 5-bit resolution for duty cycle adjustment.
Duty5Bit,
/// 6-bit resolution for duty cycle adjustment.
Duty6Bit,
/// 7-bit resolution for duty cycle adjustment.
Duty7Bit,
/// 8-bit resolution for duty cycle adjustment.
Duty8Bit,
/// 9-bit resolution for duty cycle adjustment.
Duty9Bit,
/// 10-bit resolution for duty cycle adjustment.
Duty10Bit,
/// 11-bit resolution for duty cycle adjustment.
Duty11Bit,
/// 12-bit resolution for duty cycle adjustment.
Duty12Bit,
/// 13-bit resolution for duty cycle adjustment.
Duty13Bit,
/// 14-bit resolution for duty cycle adjustment.
Duty14Bit,
#[cfg(esp32)]
/// 15-bit resolution for duty cycle adjustment.
Duty15Bit,
#[cfg(esp32)]
/// 16-bit resolution for duty cycle adjustment.
Duty16Bit,
#[cfg(esp32)]
/// 17-bit resolution for duty cycle adjustment.
Duty17Bit,
#[cfg(esp32)]
/// 18-bit resolution for duty cycle adjustment.
Duty18Bit,
#[cfg(esp32)]
/// 19-bit resolution for duty cycle adjustment.
Duty19Bit,
#[cfg(esp32)]
/// 20-bit resolution for duty cycle adjustment.
Duty20Bit,
}
impl TryFrom<u32> for Duty {
type Error = ();
fn try_from(value: u32) -> Result<Self, Self::Error> {
Ok(match value {
1 => Self::Duty1Bit,
2 => Self::Duty2Bit,
3 => Self::Duty3Bit,
4 => Self::Duty4Bit,
5 => Self::Duty5Bit,
6 => Self::Duty6Bit,
7 => Self::Duty7Bit,
8 => Self::Duty8Bit,
9 => Self::Duty9Bit,
10 => Self::Duty10Bit,
11 => Self::Duty11Bit,
12 => Self::Duty12Bit,
13 => Self::Duty13Bit,
14 => Self::Duty14Bit,
#[cfg(esp32)]
15 => Self::Duty15Bit,
#[cfg(esp32)]
16 => Self::Duty16Bit,
#[cfg(esp32)]
17 => Self::Duty17Bit,
#[cfg(esp32)]
18 => Self::Duty18Bit,
#[cfg(esp32)]
19 => Self::Duty19Bit,
#[cfg(esp32)]
20 => Self::Duty20Bit,
_ => Err(())?,
})
}
}
/// Timer configuration
#[derive(Copy, Clone)]
pub struct Config<CS> {
/// The duty cycle resolution.
pub duty: Duty,
/// The clock source for the timer.
pub clock_source: CS,
/// The frequency of the PWM signal in Hertz.
pub frequency: HertzU32,
}
}
/// Trait defining the type of timer source
pub trait TimerSpeed: Speed {
/// The type of clock source used by the timer in this speed mode.
type ClockSourceType;
}
/// Timer source type for LowSpeed timers
impl TimerSpeed for LowSpeed {
/// The clock source type for low-speed timers.
type ClockSourceType = LSClockSource;
}
#[cfg(esp32)]
/// Timer source type for HighSpeed timers
impl TimerSpeed for HighSpeed {
/// The clock source type for high-speed timers.
type ClockSourceType = HSClockSource;
}
/// Interface for Timers
pub trait TimerIFace<S: TimerSpeed> {
/// Return the frequency of the timer
fn freq(&self) -> Option<HertzU32>;
/// Configure the timer
fn configure(&mut self, config: config::Config<S::ClockSourceType>) -> Result<(), Error>;
/// Check if the timer has been configured
fn is_configured(&self) -> bool;
/// Return the duty resolution of the timer
fn duty(&self) -> Option<config::Duty>;
/// Return the timer number
fn number(&self) -> Number;
/// Return the timer frequency, or 0 if not configured
fn frequency(&self) -> u32;
}
/// Interface for HW configuration of timer
pub trait TimerHW<S: TimerSpeed> {
/// Get the current source timer frequency from the HW
fn freq_hw(&self) -> Option<HertzU32>;
/// Configure the HW for the timer
fn configure_hw(&self, divisor: u32);
/// Update the timer in HW
fn update_hw(&self);
}
/// Timer struct
pub struct Timer<'a, S: TimerSpeed> {
ledc: &'a crate::peripherals::ledc::RegisterBlock,
number: Number,
duty: Option<config::Duty>,
frequency: u32,
configured: bool,
use_ref_tick: bool,
clock_source: Option<S::ClockSourceType>,
}
impl<'a, S: TimerSpeed> TimerIFace<S> for Timer<'a, S>
where
Timer<'a, S>: TimerHW<S>,
{
/// Return the frequency of the timer
fn freq(&self) -> Option<HertzU32> {
self.freq_hw()
}
/// Configure the timer
fn configure(&mut self, config: config::Config<S::ClockSourceType>) -> Result<(), Error> {
self.duty = Some(config.duty);
self.clock_source = Some(config.clock_source);
// TODO: we should return some error here if `unwrap()` fails
let src_freq: u32 = self.freq().unwrap().to_Hz();
let precision = 1 << config.duty as u32;
let frequency: u32 = config.frequency.raw();
self.frequency = frequency;
let mut divisor = ((src_freq as u64) << 8) / frequency as u64 / precision as u64;
if divisor > LEDC_TIMER_DIV_NUM_MAX {
// APB_CLK results in divisor which too high. Try using REF_TICK as clock
// source.
self.use_ref_tick = true;
divisor = (1_000_000u64 << 8) / frequency as u64 / precision as u64;
}
if !(256..LEDC_TIMER_DIV_NUM_MAX).contains(&divisor) {
return Err(Error::Divisor);
}
self.configure_hw(divisor as u32);
self.update_hw();
self.configured = true;
Ok(())
}
/// Check if the timer has been configured
fn is_configured(&self) -> bool {
self.configured
}
/// Return the duty resolution of the timer
fn duty(&self) -> Option<config::Duty> {
self.duty
}
/// Return the timer number
fn number(&self) -> Number {
self.number
}
/// Return the timer frequency
fn frequency(&self) -> u32 {
self.frequency
}
}
impl<'a, S: TimerSpeed> Timer<'a, S> {
/// Create a new instance of a timer
pub fn new(ledc: &'a ledc::RegisterBlock, number: Number) -> Self {
Timer {
ledc,
number,
duty: None,
frequency: 0u32,
configured: false,
use_ref_tick: false,
clock_source: None,
}
}
}
/// Timer HW implementation for LowSpeed timers
impl TimerHW<LowSpeed> for Timer<'_, LowSpeed> {
/// Get the current source timer frequency from the HW
fn freq_hw(&self) -> Option<HertzU32> {
self.clock_source.map(|source| match source {
LSClockSource::APBClk => {
let clocks = Clocks::get();
clocks.apb_clock
}
})
}
#[cfg(esp32)]
/// Configure the HW for the timer
fn configure_hw(&self, divisor: u32) {
let duty = unwrap!(self.duty) as u8;
let use_apb = !self.use_ref_tick;
self.ledc
.lstimer(self.number as usize)
.conf()
.modify(|_, w| unsafe {
w.tick_sel().bit(use_apb);
w.rst().clear_bit();
w.pause().clear_bit();
w.div_num().bits(divisor);
w.duty_res().bits(duty)
});
}
#[cfg(not(esp32))]
/// Configure the HW for the timer
fn configure_hw(&self, divisor: u32) {
let duty = unwrap!(self.duty) as u8;
let use_ref_tick = self.use_ref_tick;
self.ledc
.timer(self.number as usize)
.conf()
.modify(|_, w| unsafe {
w.tick_sel().bit(use_ref_tick);
w.rst().clear_bit();
w.pause().clear_bit();
w.clk_div().bits(divisor);
w.duty_res().bits(duty)
});
}
/// Update the timer in HW
fn update_hw(&self) {
cfg_if::cfg_if! {
if #[cfg(esp32)] {
let tmr = self.ledc.lstimer(self.number as usize);
} else {
let tmr = self.ledc.timer(self.number as usize);
}
}
tmr.conf().modify(|_, w| w.para_up().set_bit());
}
}
#[cfg(esp32)]
/// Timer HW implementation for HighSpeed timers
impl<'a> TimerHW<HighSpeed> for Timer<'a, HighSpeed> {
/// Get the current source timer frequency from the HW
fn freq_hw(&self) -> Option<HertzU32> {
self.clock_source.map(|source| match source {
HSClockSource::APBClk => {
let clocks = Clocks::get();
clocks.apb_clock
}
})
}
/// Configure the HW for the timer
fn configure_hw(&self, divisor: u32) {
let duty = unwrap!(self.duty) as u8;
let sel_hstimer = self.clock_source == Some(HSClockSource::APBClk);
self.ledc
.hstimer(self.number as usize)
.conf()
.modify(|_, w| unsafe {
w.tick_sel().bit(sel_hstimer);
w.rst().clear_bit();
w.pause().clear_bit();
w.div_num().bits(divisor);
w.duty_res().bits(duty)
});
}
/// Update the timer in HW
fn update_hw(&self) {
// Nothing to do for HS timers
}
}