lilkaoxide/
buzzer.rs

1use crate::cfg::BuzzerConfig;
2use esp_hal::gpio::{DriveMode, interconnect::PeripheralOutput};
3use esp_hal::ledc::{
4    LSGlobalClkSource, Ledc, LowSpeed, channel, channel::ChannelIFace, timer, timer::TimerIFace,
5};
6use esp_hal::peripherals::LEDC;
7use static_cell::StaticCell;
8
9type BuzzerTimer = timer::Timer<'static, LowSpeed>;
10type BuzzerChannel = channel::Channel<'static, LowSpeed>;
11
12static BUZZER_TIMER: StaticCell<BuzzerTimer> = StaticCell::new();
13
14/// Error returned when the LEDC buzzer fails to initialise.
15///
16/// Returned by the internal `init_buzzer` function and propagated through
17/// [`crate::InitError::Buzzer`] from [`crate::Lilka::init`].
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum BuzzerInitError {
20    /// LEDC timer configuration failed (unsupported frequency / duty combination).
21    Timer(timer::Error),
22    /// LEDC channel configuration failed (pin conflict or unsupported drive mode).
23    Channel(channel::Error),
24}
25
26impl BuzzerInitError {
27    /// Returns a short human-readable description of the error, suitable for `defmt` logging.
28    pub const fn as_str(self) -> &'static str {
29        match self {
30            Self::Timer(_) => "Buzzer timer init failed",
31            Self::Channel(_) => "Buzzer channel init failed",
32        }
33    }
34}
35
36/// PWM buzzer driver backed by the ESP32-S3 LEDC peripheral.
37///
38/// Created automatically by [`crate::Lilka::init`] and available as
39/// [`crate::Lilka::buzzer`].  The buzzer is connected to **GPIO11** via the
40/// LEDC low-speed channel 0, timer 0.
41///
42/// # Changing frequency at runtime
43///
44/// The public [`Buzzer::channel`] field exposes the raw LEDC channel.  You can
45/// reconfigure it at any time using the `ChannelIFace` trait from `esp-hal`:
46///
47/// ```rust,no_run
48/// use lilkaoxide::prelude::*;
49/// use esp_hal::ledc::{channel, channel::ChannelIFace};
50///
51/// # fn example(mut lilka: Lilka) {
52/// // Re-use the existing timer but change duty to 0 (silence)
53/// lilka.buzzer.channel.set_duty_hw(0);
54/// # }
55/// ```
56///
57/// # Turning the buzzer on/off
58///
59/// Set the duty to 0 to silence it without stopping the LEDC clock:
60///
61/// ```rust,no_run
62/// use lilkaoxide::prelude::*;
63/// use esp_hal::ledc::channel::ChannelIFace;
64///
65/// # fn beep(mut lilka: Lilka) {
66/// // On
67/// lilka.buzzer.channel.set_duty_hw(128); // ~50% of 8-bit duty
68/// // Off
69/// lilka.buzzer.channel.set_duty_hw(0);
70/// # }
71/// ```
72pub struct Buzzer {
73    /// The underlying LEDC channel (low-speed, channel 0).
74    /// Use [`esp_hal::ledc::channel::ChannelIFace`] methods to control the output at runtime.
75    pub channel: BuzzerChannel,
76}
77
78pub fn init_buzzer(
79    config: BuzzerConfig,
80    ledc_peripheral: LEDC<'static>,
81    buzzer_pin: impl PeripheralOutput<'static>,
82) -> Result<Buzzer, BuzzerInitError> {
83    let mut ledc = Ledc::new(ledc_peripheral);
84    ledc.set_global_slow_clock(LSGlobalClkSource::APBClk);
85
86    let timer = BUZZER_TIMER.init(ledc.timer::<LowSpeed>(timer::Number::Timer0));
87    timer
88        .configure(timer::config::Config {
89            duty: config.timer_duty,
90            clock_source: timer::LSClockSource::APBClk,
91            frequency: config.frequency,
92        })
93        .map_err(BuzzerInitError::Timer)?;
94
95    let mut channel = ledc.channel::<LowSpeed>(channel::Number::Channel0, buzzer_pin);
96    channel
97        .configure(channel::config::Config {
98            timer,
99            duty_pct: config.duty_pct.min(100),
100            drive_mode: DriveMode::PushPull,
101        })
102        .map_err(BuzzerInitError::Channel)?;
103
104    Ok(Buzzer { channel })
105}