Skip to main content

oxstack_cardputer/
backlight.rs

1//! LCD backlight on GPIO38, dimmed with LEDC PWM.
2//!
3//! **GPIO38 also switches power to the RGB LED.** A WS2812 loses its state
4//! whenever its supply drops, so any dimming below full brightness chops the
5//! LED's power at the PWM rate. Keep the backlight at [`Backlight::MAX`]
6//! while using the RGB LED. *(Inferred from the schematic and M5Stack's docs;
7//! not yet measured on hardware.)*
8//!
9//! The driver uses LEDC low-speed timer 0 and channel 0. Dropping a
10//! [`Backlight`] leaves the PWM running at its last level.
11
12use embassy_sync::once_lock::OnceLock;
13use esp_hal::gpio::DriveMode;
14use esp_hal::ledc::channel::{self, Channel, ChannelHW, ChannelIFace};
15use esp_hal::ledc::timer::{self, LSClockSource, Timer, TimerIFace};
16use esp_hal::ledc::{LSGlobalClkSource, Ledc, LowSpeed};
17use esp_hal::peripherals::{GPIO38, LEDC};
18use esp_hal::time::Rate;
19
20/// PWM frequency: well above visible flicker and within 8-bit LEDC range
21/// from the 80 MHz APB clock.
22pub const PWM_FREQUENCY: Rate = Rate::from_khz(2);
23
24/// LEDC peripheral and the backlight pin.
25#[allow(missing_docs)]
26pub struct BacklightResources<'d> {
27    pub ledc: LEDC<'d>,
28    pub pin: GPIO38<'d>,
29}
30reborrow_group!(BacklightResources { ledc, pin });
31
32/// The LEDC timer, configured once and shared by every [`Backlight`].
33///
34/// A `Channel` holds a reference to its timer for its whole life, so the
35/// timer must outlive any `Backlight`: it lives in a static.
36struct SharedTimer(Timer<'static, LowSpeed>);
37
38// SAFETY: after `configure` (which runs once, before the value is
39// published) the timer is only read through `&self`: its fields are plain
40// values and the register block is accessed by volatile MMIO reads.
41unsafe impl Sync for SharedTimer {}
42
43fn timer() -> &'static Timer<'static, LowSpeed> {
44    static TIMER: OnceLock<SharedTimer> = OnceLock::new();
45    &TIMER
46        .get_or_init(|| {
47            // SAFETY: `BacklightResources` owns the real `LEDC` token and is
48            // borrowed for the life of every `Backlight`; this alias only
49            // configures timer 0, which the token's owner is told not to use.
50            let mut ledc = Ledc::new(unsafe { LEDC::steal() });
51            ledc.set_global_slow_clock(LSGlobalClkSource::APBClk);
52            let mut t = ledc.timer::<LowSpeed>(timer::Number::Timer0);
53            t.configure(timer::config::Config {
54                duty: timer::config::Duty::Duty8Bit,
55                clock_source: LSClockSource::APBClk,
56                frequency: PWM_FREQUENCY,
57            })
58            .expect("2 kHz at 8 bits is reachable from APB");
59            SharedTimer(t)
60        })
61        .0
62}
63
64impl<'d> BacklightResources<'d> {
65    /// Starts the backlight at full brightness.
66    pub fn init(&mut self) -> Backlight<'_> {
67        let mut ch = Channel::<LowSpeed>::new(channel::Number::Channel0, self.pin.reborrow());
68        ch.configure(channel::config::Config {
69            timer: timer(),
70            duty_pct: 100,
71            drive_mode: DriveMode::PushPull,
72        })
73        .expect("valid LEDC channel configuration");
74        let mut backlight = Backlight {
75            ch,
76            level: Backlight::MAX,
77        };
78        backlight.set(Backlight::MAX);
79        backlight
80    }
81}
82
83/// Backlight brightness control.
84pub struct Backlight<'d> {
85    ch: Channel<'d, LowSpeed>,
86    level: u8,
87}
88
89impl Backlight<'_> {
90    /// Full brightness: the pin is held steadily high (no PWM).
91    pub const MAX: u8 = u8::MAX;
92
93    /// Sets brightness, `0` (off) to [`Self::MAX`] (steady on).
94    pub fn set(&mut self, level: u8) {
95        // An 8-bit LEDC duty of 256 means "always high", so map 255 to 256
96        // to keep full brightness free of PWM edges.
97        let duty = if level == Self::MAX {
98            1 << 8
99        } else {
100            u32::from(level)
101        };
102        self.ch.set_duty_hw(duty);
103        self.level = level;
104    }
105
106    /// Current brightness.
107    pub fn level(&self) -> u8 {
108        self.level
109    }
110}