aboutsummaryrefslogtreecommitdiff
path: root/embassy-stm32/src/lcd.rs
diff options
context:
space:
mode:
Diffstat (limited to 'embassy-stm32/src/lcd.rs')
-rw-r--r--embassy-stm32/src/lcd.rs510
1 files changed, 510 insertions, 0 deletions
diff --git a/embassy-stm32/src/lcd.rs b/embassy-stm32/src/lcd.rs
new file mode 100644
index 000000000..ea29f1398
--- /dev/null
+++ b/embassy-stm32/src/lcd.rs
@@ -0,0 +1,510 @@
1//! LCD
2use core::marker::PhantomData;
3
4use embassy_hal_internal::{Peri, PeripheralType};
5
6use crate::gpio::{AfType, AnyPin, SealedPin};
7use crate::peripherals;
8use crate::rcc::{self, RccPeripheral};
9use crate::time::Hertz;
10
11#[cfg(any(stm32u0, stm32l073, stm32l083))]
12const NUM_SEGMENTS: u8 = 52;
13#[cfg(any(stm32wb, stm32l4x6, stm32l15x, stm32l162, stm32l4x3, stm32l4x6))]
14const NUM_SEGMENTS: u8 = 44;
15#[cfg(any(stm32l053, stm32l063, stm32l100))]
16const NUM_SEGMENTS: u8 = 32;
17
18/// LCD configuration struct
19#[non_exhaustive]
20#[derive(Debug, Clone, Copy)]
21pub struct Config {
22 #[cfg(lcd_v2)]
23 /// Enable the voltage output buffer for higher driving capability.
24 ///
25 /// The LCD driving capability is improved as buffers prevent the LCD capacitive loads from loading the resistor
26 /// bridge unacceptably and interfering with its voltage generation.
27 pub use_voltage_output_buffer: bool,
28 /// Enable SEG pin remapping. SEG[31:28] multiplexed with SEG[43:40]
29 pub use_segment_muxing: bool,
30 /// Bias selector
31 pub bias: Bias,
32 /// Duty selector
33 pub duty: Duty,
34 /// Internal or external voltage source
35 pub voltage_source: VoltageSource,
36 /// The frequency used to update the LCD with.
37 /// Should be between ~30 and ~100. Lower is better for power consumption, but has lower visual fidelity.
38 pub target_fps: Hertz,
39 /// LCD driver selector
40 pub drive: Drive,
41}
42
43impl Default for Config {
44 fn default() -> Self {
45 Self {
46 #[cfg(lcd_v2)]
47 use_voltage_output_buffer: false,
48 use_segment_muxing: false,
49 bias: Default::default(),
50 duty: Default::default(),
51 voltage_source: Default::default(),
52 target_fps: Hertz(60),
53 drive: Drive::Medium,
54 }
55 }
56}
57
58/// The number of voltage levels used when driving an LCD.
59/// Your LCD datasheet should tell you what to use.
60#[repr(u8)]
61#[derive(Debug, Default, Clone, Copy)]
62pub enum Bias {
63 /// 1/4 bias
64 #[default]
65 Quarter = 0b00,
66 /// 1/2 bias
67 Half = 0b01,
68 /// 1/3 bias
69 Third = 0b10,
70}
71
72/// The duty used by the LCD driver.
73///
74/// This is essentially how many COM pins you're using.
75#[repr(u8)]
76#[derive(Debug, Default, Clone, Copy)]
77pub enum Duty {
78 #[default]
79 /// Use a single COM pin
80 Static = 0b000,
81 /// Use two COM pins
82 Half = 0b001,
83 /// Use three COM pins
84 Third = 0b010,
85 /// Use four COM pins
86 Quarter = 0b011,
87 /// Use eight COM pins.
88 ///
89 /// In this mode, `COM[7:4]` outputs are available on `SEG[51:48]`.
90 /// This allows reducing the number of available segments.
91 Eigth = 0b100,
92}
93
94impl Duty {
95 fn num_com_pins(&self) -> u8 {
96 match self {
97 Duty::Static => 1,
98 Duty::Half => 2,
99 Duty::Third => 3,
100 Duty::Quarter => 4,
101 Duty::Eigth => 8,
102 }
103 }
104}
105
106/// Whether to use the internal or external voltage source to drive the LCD
107#[repr(u8)]
108#[derive(Debug, Default, Clone, Copy)]
109pub enum VoltageSource {
110 #[default]
111 /// Voltage stepup converter
112 Internal,
113 /// VLCD pin
114 External,
115}
116
117/// Defines the pulse duration in terms of ck_ps pulses.
118///
119/// A short pulse leads to lower power consumption, but displays with high internal resistance
120/// may need a longer pulse to achieve satisfactory contrast.
121/// Note that the pulse is never longer than one half prescaled LCD clock period.
122///
123/// Displays with high internal resistance may need a longer drive time to achieve satisfactory contrast.
124/// `PermanentHighDrive` is useful in this case if some additional power consumption can be tolerated.
125///
126/// Basically, for power usage, you want this as low as possible while still being able to use the LCD
127/// with a good enough contrast.
128#[repr(u8)]
129#[derive(Debug, Clone, Copy)]
130pub enum Drive {
131 /// Zero clock pulse on duration
132 Lowest = 0x00,
133 /// One clock pulse on duration
134 VeryLow = 0x01,
135 /// Two clock pulse on duration
136 Low = 0x02,
137 /// Three clock pulse on duration
138 Medium = 0x03,
139 /// Four clock pulse on duration
140 MediumHigh = 0x04,
141 /// Five clock pulse on duration
142 High = 0x05,
143 /// Six clock pulse on duration
144 VeryHigh = 0x06,
145 /// Seven clock pulse on duration
146 Highest = 0x07,
147 /// Enables the highdrive bit of the hardware
148 PermanentHighDrive = 0x09,
149}
150
151/// LCD driver.
152pub struct Lcd<'d, T: Instance> {
153 _peri: PhantomData<&'d mut T>,
154 duty: Duty,
155 ck_div: u32,
156}
157
158impl<'d, T: Instance> Lcd<'d, T> {
159 /// Initialize the lcd driver.
160 ///
161 /// The `pins` parameter must contain *all* segment and com pins that are connected to the LCD.
162 /// This is not further checked by this driver. Pins not routed to the LCD can be used for other purposes.
163 pub fn new<const N: usize>(
164 _peripheral: Peri<'d, T>,
165 config: Config,
166 vlcd_pin: Peri<'_, impl VlcdPin<T>>,
167 pins: [LcdPin<'d, T>; N],
168 ) -> Self {
169 rcc::enable_and_reset::<T>();
170
171 vlcd_pin.set_as_af(
172 vlcd_pin.af_num(),
173 AfType::output(crate::gpio::OutputType::PushPull, crate::gpio::Speed::VeryHigh),
174 );
175
176 assert_eq!(
177 pins.iter().filter(|pin| !pin.is_seg).count(),
178 config.duty.num_com_pins() as usize,
179 "The number of provided COM pins is not the same as the duty configures"
180 );
181
182 // Set the pins
183 for pin in pins {
184 pin.pin.set_as_af(
185 pin.af_num,
186 AfType::output(crate::gpio::OutputType::PushPull, crate::gpio::Speed::VeryHigh),
187 );
188 }
189
190 // Initialize the display ram to 0
191 for i in 0..8 {
192 T::regs().ram_com(i).low().write_value(0);
193 T::regs().ram_com(i).high().write_value(0);
194 }
195
196 // Calculate the clock dividers
197 let Some(lcd_clk) = (unsafe { rcc::get_freqs().rtc.to_hertz() }) else {
198 panic!("The LCD driver needs the RTC/LCD clock to be running");
199 };
200 let duty_divider = match config.duty {
201 Duty::Static => 1,
202 Duty::Half => 2,
203 Duty::Third => 3,
204 Duty::Quarter => 4,
205 Duty::Eigth => 8,
206 };
207 let target_clock = config.target_fps.0 * duty_divider;
208 let target_division = lcd_clk.0 / target_clock;
209
210 let mut ps = 0;
211 let mut div = 0;
212 let mut best_fps_match = u32::MAX;
213
214 for trial_div in 0..0xF {
215 let trial_ps = (target_division / (trial_div + 16))
216 .next_power_of_two()
217 .trailing_zeros();
218 let fps = lcd_clk.0 / ((1 << trial_ps) * (trial_div + 16)) / duty_divider;
219
220 if fps < config.target_fps.0 {
221 continue;
222 }
223
224 if fps < best_fps_match {
225 ps = trial_ps;
226 div = trial_div;
227 best_fps_match = fps;
228 }
229 }
230
231 let ck_div = lcd_clk.0 / ((1 << ps) * (div + 16));
232
233 trace!(
234 "lcd_clk: {}, fps: {}, ps: {}, div: {}, ck_div: {}",
235 lcd_clk, best_fps_match, ps, div, ck_div
236 );
237
238 if best_fps_match == u32::MAX || ps > 0xF {
239 panic!("Lcd clock error");
240 }
241
242 // Set the frame control
243 T::regs().fcr().modify(|w| {
244 w.set_ps(ps as u8);
245 w.set_div(div as u8);
246 w.set_cc(0b100); // Init in the middle-ish
247 w.set_dead(0b000);
248 w.set_pon(config.drive as u8 & 0x07);
249 w.set_hd((config.drive as u8 & !0x07) != 0);
250 });
251
252 // Wait for the frame control to synchronize
253 while !T::regs().sr().read().fcrsf() {}
254
255 // Set the control register values
256 T::regs().cr().modify(|w| {
257 #[cfg(lcd_v2)]
258 w.set_bufen(config.use_voltage_output_buffer);
259 w.set_mux_seg(config.use_segment_muxing);
260 w.set_bias(config.bias as u8);
261 w.set_duty(config.duty as u8);
262 w.set_vsel(matches!(config.voltage_source, VoltageSource::External));
263 });
264
265 // Enable the lcd
266 T::regs().cr().modify(|w| w.set_lcden(true));
267
268 // Wait for the lcd to be enabled
269 while !T::regs().sr().read().ens() {}
270
271 // Wait for the stepup converter to be ready
272 while !T::regs().sr().read().rdy() {}
273
274 Self {
275 _peri: PhantomData,
276 duty: config.duty,
277 ck_div,
278 }
279 }
280
281 /// Change the contrast by changing the voltage being used.
282 ///
283 /// This is from low at 0 to high at 7.
284 pub fn set_contrast_control(&mut self, value: u8) {
285 assert!((0..=7).contains(&value));
286 T::regs().fcr().modify(|w| w.set_cc(value));
287 }
288
289 /// Change the contrast by introducing a deadtime to the signals
290 /// where the voltages are held at 0V.
291 ///
292 /// This is from no dead time at 0 to high dead time at 7.
293 pub fn set_dead_time(&mut self, value: u8) {
294 assert!((0..=7).contains(&value));
295 T::regs()
296 .fcr()
297 .modify(|w: &mut stm32_metapac::lcd::regs::Fcr| w.set_dead(value));
298 }
299
300 /// Write data into the display RAM. This overwrites the data already in it for the specified com index.
301 ///
302 /// The `com_index` value determines which part of the RAM is written to.
303 /// The `segments` value is a bitmap where each bit represents whether a pixel is turned on or off.
304 ///
305 /// This function waits last update request to be finished, but does not submit the buffer to the LCD with a new request.
306 /// Submission has to be done manually using [Self::submit_frame].
307 pub fn write_com_segments(&mut self, com_index: u8, segments: u64) {
308 while T::regs().sr().read().udr() {}
309
310 assert!(
311 com_index < self.duty.num_com_pins(),
312 "Com index cannot be higher than number of configured com pins (through the Duty setting in the config)"
313 );
314
315 assert!(
316 segments.leading_zeros() >= 64 - self.num_segments() as u32,
317 "Invalid segment pixel set",
318 );
319
320 T::regs()
321 .ram_com(com_index as usize)
322 .low()
323 .write_value((segments & 0xFFFF_FFFF) as u32);
324 T::regs()
325 .ram_com(com_index as usize)
326 .high()
327 .write_value(((segments >> 32) & 0xFFFF_FFFF) as u32);
328 }
329
330 /// Read the data from the display RAM.
331 ///
332 /// The `com_index` value determines which part of the RAM is read from.
333 ///
334 /// This function waits for the last update request to be finished.
335 pub fn read_com_segments(&self, com_index: u8) -> u64 {
336 while T::regs().sr().read().udr() {}
337
338 assert!(
339 com_index < self.duty.num_com_pins(),
340 "Com index cannot be higher than number of configured com pins (through the Duty setting in the config)"
341 );
342
343 let low = T::regs().ram_com(com_index as usize).low().read();
344 let high = T::regs().ram_com(com_index as usize).high().read();
345
346 ((high as u64) << 32) | low as u64
347 }
348
349 /// Submit the current RAM data to the LCD.
350 ///
351 /// This function waits until the RAM is writable, but does not wait for the frame to be drawn.
352 pub fn submit_frame(&mut self) {
353 while T::regs().sr().read().udr() {}
354 // Clear the update done flag
355 T::regs().sr().write(|w| w.set_udd(true));
356 // Set the update request flag
357 T::regs().sr().write(|w| w.set_udr(true));
358 }
359
360 /// Get the number of segments that are supported on this LCD
361 pub fn num_segments(&self) -> u8 {
362 match self.duty {
363 Duty::Eigth => NUM_SEGMENTS - 4, // With 8 coms, 4 of the segment pins turn into com pins
364 _ => NUM_SEGMENTS,
365 }
366 }
367
368 /// Get the pixel mask for the current LCD setup.
369 /// This is a mask of all bits that are allowed to be set in the [Self::write_com_segments] function.
370 pub fn segment_pixel_mask(&self) -> u64 {
371 (1 << self.num_segments()) - 1
372 }
373
374 /// Get the number of COM pins that were configured through the Drive config
375 pub fn num_com_pins(&self) -> u8 {
376 self.duty.num_com_pins()
377 }
378
379 /// Set the blink behavior on some pixels.
380 ///
381 /// The blink frequency is an approximation. It's divided from the clock selected by the FPS.
382 /// Play with the FPS value if you want the blink frequency to be more accurate.
383 ///
384 /// If a blink frequency cannot be attained, this function will panic.
385 pub fn set_blink(&mut self, selector: BlinkSelector, freq: BlinkFreq) {
386 // Freq * 100 to be able to do integer math
387 let scaled_blink_freq = match freq {
388 BlinkFreq::Hz0_25 => 25,
389 BlinkFreq::Hz0_5 => 50,
390 BlinkFreq::Hz1 => 100,
391 BlinkFreq::Hz2 => 200,
392 BlinkFreq::Hz4 => 400,
393 };
394
395 let desired_divider = self.ck_div * 100 / scaled_blink_freq;
396 let target_divider = desired_divider.next_power_of_two();
397 let power_divisions = target_divider.trailing_zeros();
398
399 trace!(
400 "Setting LCD blink frequency -> desired_divider: {}, target_divider: {}",
401 desired_divider, target_divider
402 );
403
404 assert!(
405 (8..=1024).contains(&target_divider),
406 "LCD blink frequency cannot be attained"
407 );
408
409 T::regs().fcr().modify(|reg| {
410 reg.set_blinkf((power_divisions - 3) as u8);
411 reg.set_blink(selector as u8);
412 })
413 }
414}
415
416impl<'d, T: Instance> Drop for Lcd<'d, T> {
417 fn drop(&mut self) {
418 // Disable the lcd
419 T::regs().cr().modify(|w| w.set_lcden(false));
420 rcc::disable::<T>();
421 }
422}
423
424/// Blink frequency
425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
426pub enum BlinkFreq {
427 /// 0.25 hz
428 Hz0_25,
429 /// 0.5 hz
430 Hz0_5,
431 /// 1 hz
432 Hz1,
433 /// 2 hz
434 Hz2,
435 /// 4 hz
436 Hz4,
437}
438
439/// Blink pixel selector
440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441#[repr(u8)]
442pub enum BlinkSelector {
443 /// No pixels blink
444 None = 0b00,
445 /// The SEG0, COM0 pixel blinks if the pixel is set
446 Seg0Com0 = 0b01,
447 /// The SEG0 pixel of all COMs blinks if the pixel is set
448 Seg0ComAll = 0b10,
449 /// All pixels blink if the pixel is set
450 All = 0b11,
451}
452
453/// A type-erased pin that can be configured as an LCD pin.
454/// This is used for passing pins to the new function in the array.
455pub struct LcdPin<'d, T: Instance> {
456 pin: Peri<'d, AnyPin>,
457 af_num: u8,
458 is_seg: bool,
459 _phantom: PhantomData<T>,
460}
461
462impl<'d, T: Instance> LcdPin<'d, T> {
463 /// Construct an LCD pin from any pin that supports it
464 pub fn new_seg(pin: Peri<'d, impl SegPin<T>>) -> Self {
465 let af = pin.af_num();
466
467 Self {
468 pin: pin.into(),
469 af_num: af,
470 is_seg: true,
471 _phantom: PhantomData,
472 }
473 }
474
475 /// Construct an LCD pin from any pin that supports it
476 pub fn new_com(pin: Peri<'d, impl ComPin<T>>) -> Self {
477 let af = pin.af_num();
478
479 Self {
480 pin: pin.into(),
481 af_num: af,
482 is_seg: false,
483 _phantom: PhantomData,
484 }
485 }
486}
487
488trait SealedInstance: crate::rcc::SealedRccPeripheral + PeripheralType {
489 fn regs() -> crate::pac::lcd::Lcd;
490}
491
492/// DSI instance trait.
493#[allow(private_bounds)]
494pub trait Instance: SealedInstance + RccPeripheral + 'static {}
495
496pin_trait!(SegPin, Instance);
497pin_trait!(ComPin, Instance);
498pin_trait!(VlcdPin, Instance);
499
500foreach_peripheral!(
501 (lcd, $inst:ident) => {
502 impl crate::lcd::SealedInstance for peripherals::$inst {
503 fn regs() -> crate::pac::lcd::Lcd {
504 crate::pac::$inst
505 }
506 }
507
508 impl crate::lcd::Instance for peripherals::$inst {}
509 };
510);