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}