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}