lcd1602_driver/
lcd.rs

1//! [`Lcd`] is the main driver for LCD1602
2
3use embedded_hal::delay::DelayNs;
4
5use crate::{
6    command::{Font, LineMode, MoveDirection, RAMType, ShiftType, State},
7    state::LcdState,
8};
9
10mod init;
11
12pub use init::Config;
13
14mod impls;
15
16/// [`Lcd`] is the main struct to drive a LCD1602
17pub struct Lcd<'a, 'b, Sender, Delayer, const READABLE: bool>
18where
19    Delayer: DelayNs,
20{
21    sender: &'a mut Sender,
22    delayer: &'b mut Delayer,
23    state: LcdState,
24    poll_interval_us: u32,
25}
26
27/// [`CGRAMGraph`] represent a graph inside a CGRAM, that can be draw on screen
28#[derive(Default)]
29pub struct CGRAMGraph {
30    /// This is the upper part of the graph.  
31    /// It shows for both 5x8 and 5x11 Font
32    pub upper: [u8; 8],
33    /// This is the lower part of the graph.  
34    /// It shows only 5x11 Font.
35    ///
36    /// Although this part is only 3 u8, but in LCD hardware, it will occupied a full graph point.
37    pub lower: Option<[u8; 3]>,
38}
39
40/// All basic command to control LCD1602
41#[allow(missing_docs)]
42pub trait Basic {
43    fn write_byte_to_cur(&mut self, byte: u8);
44
45    /// Note:  
46    /// We allow write Font5x11 graph even under Font5x8 mode.
47    fn write_graph_to_cgram(&mut self, index: u8, graph_data: &CGRAMGraph);
48
49    fn clean_display(&mut self);
50
51    fn return_home(&mut self);
52
53    fn set_line_mode(&mut self, line: LineMode);
54
55    fn get_line_mode(&self) -> LineMode;
56
57    fn set_font(&mut self, font: Font);
58
59    fn get_font(&self) -> Font;
60
61    fn set_display_state(&mut self, display: State);
62
63    fn get_display_state(&self) -> State;
64
65    fn set_cursor_state(&mut self, cursor: State);
66
67    fn get_cursor_state(&self) -> State;
68
69    fn get_ram_type(&self) -> RAMType;
70
71    fn set_cursor_blink_state(&mut self, blink: State);
72
73    fn get_cursor_blink_state(&self) -> State;
74
75    fn set_direction(&mut self, dir: MoveDirection);
76
77    fn get_direction(&self) -> MoveDirection;
78
79    fn set_shift_type(&mut self, shift: ShiftType);
80
81    fn get_shift_type(&self) -> ShiftType;
82
83    fn set_cursor_pos(&mut self, pos: (u8, u8));
84
85    fn set_cgram_addr(&mut self, addr: u8);
86
87    fn get_cursor_pos(&self) -> (u8, u8);
88
89    fn shift_cursor_or_display(&mut self, shift_type: ShiftType, dir: MoveDirection);
90
91    fn get_display_offset(&self) -> u8;
92
93    fn set_poll_interval(&mut self, interval_us: u32);
94
95    fn get_poll_interval_us(&self) -> u32;
96
97    fn get_line_capacity(&self) -> u8;
98
99    /// Note:
100    /// Due to driver implementation, this function may have actual effect, or not
101    fn set_backlight(&mut self, backlight: State);
102
103    fn get_backlight(&self) -> State;
104
105    fn calculate_pos_by_offset(&self, start: (u8, u8), offset: (i8, i8)) -> (u8, u8);
106
107    /// Wait for specified milliseconds
108    fn delay_ms(&mut self, ms: u32);
109
110    /// Wait for specified microseconds
111    fn delay_us(&mut self, us: u32);
112}
113
114/// Basic read functions for the LCD
115#[allow(missing_docs)]
116pub trait BasicRead: Basic {
117    fn read_u8_from_cur(&mut self) -> u8;
118}
119
120/// Useful command to control LCD1602
121pub trait Ext: Basic {
122    /// toggle entire display on and off (it doesn't toggle backlight)
123    fn toggle_display(&mut self) {
124        match self.get_display_state() {
125            State::Off => self.set_display_state(State::On),
126            State::On => self.set_display_state(State::Off),
127        }
128    }
129
130    /// write [char] to current position
131    /// In default implementation, character only support
132    /// from ASCII 0x20 (white space) to ASCII 0x7D (`}`)
133    fn write_char_to_cur(&mut self, char: char) {
134        assert!(
135            self.get_ram_type() == RAMType::DDRam,
136            "Current in CGRAM, use .set_cursor_pos() to change to DDRAM"
137        );
138
139        // map char out side of ASCII 0x20 and 0x7D to full rectangle
140        let out_byte = match char.is_ascii() {
141            true if (0x20 <= char as u8) && (char as u8 <= 0x7D) => char as u8,
142            _ => 0xFF,
143        };
144
145        self.write_byte_to_cur(out_byte);
146    }
147
148    /// write string to current position
149    fn write_str_to_cur(&mut self, str: &str) {
150        str.chars().for_each(|char| self.write_char_to_cur(char));
151    }
152
153    /// write a byte to specific position
154    fn write_byte_to_pos(&mut self, byte: u8, pos: (u8, u8)) {
155        self.set_cursor_pos(pos);
156
157        self.write_byte_to_cur(byte);
158    }
159
160    /// write a char to specific position
161    fn write_char_to_pos(&mut self, char: char, pos: (u8, u8)) {
162        self.set_cursor_pos(pos);
163        self.write_char_to_cur(char);
164    }
165
166    /// write string to specific position
167    fn write_str_to_pos(&mut self, str: &str, pos: (u8, u8)) {
168        self.set_cursor_pos(pos);
169        self.write_str_to_cur(str);
170    }
171
172    /// write custom graph to current position
173    ///
174    /// If you write 5x11 Font graph, but only want to access upper part of the graph in 5x8 Font mode,  
175    /// you will need to shift `index` one bit left to get correct graph.
176    fn write_graph_to_cur(&mut self, index: u8) {
177        match self.get_font() {
178            Font::Font5x8 => assert!(index < 8, "index too big, should less than 8 for 5x8 Font"),
179            Font::Font5x11 => assert!(index < 4, "index too big, should less than 4 for 5x11 Font"),
180        }
181
182        self.write_byte_to_cur(match self.get_font() {
183            Font::Font5x8 => index,
184            Font::Font5x11 => index << 1,
185        });
186    }
187
188    /// write custom graph to specific position
189    ///
190    /// If you write 5x11 Font graph, but only want to access upper part of the graph in 5x8 Font mode,  
191    /// you will need to shift `index` one bit left to get correct graph.
192    fn write_graph_to_pos(&mut self, index: u8, pos: (u8, u8)) {
193        match self.get_font() {
194            Font::Font5x8 => assert!(index < 8, "Only 8 graphs allowed in CGRAM for 5x8 Font"),
195            Font::Font5x11 => assert!(index < 4, "Only 4 graphs allowed in CGRAM for 5x11 Font"),
196        }
197
198        self.write_byte_to_pos(
199            match self.get_font() {
200                Font::Font5x8 => index,
201                Font::Font5x11 => index << 1,
202            },
203            pos,
204        );
205    }
206
207    /// change cursor position with relative offset
208    fn offset_cursor_pos(&mut self, offset: (i8, i8)) {
209        self.set_cursor_pos(self.calculate_pos_by_offset(self.get_cursor_pos(), offset));
210    }
211}
212
213/// Useful commands to read data from the LCD
214pub trait ExtRead: Ext + BasicRead {
215    /// read a byte from specific position
216    fn read_byte_from_pos(&mut self, pos: (u8, u8)) -> u8 {
217        let original_pos = self.get_cursor_pos();
218        self.set_cursor_pos(pos);
219        let data = self.read_u8_from_cur();
220        self.set_cursor_pos(original_pos);
221        data
222    }
223
224    /// read custom graph data from CGRAM
225    ///
226    /// We always read graph data as Font 5x11 mode,  
227    /// user will take response to check whether the second part is needed.
228    fn read_graph_from_cgram(&mut self, index: u8) -> CGRAMGraph {
229        match self.get_font() {
230            Font::Font5x8 => assert!(index < 8, "index too big, should less than 8 for 5x8 Font"),
231            Font::Font5x11 => assert!(index < 4, "index too big, should less than 4 for 5x11 Font"),
232        }
233
234        // convert index to cgram address
235        self.set_cgram_addr(
236            index
237                .checked_shl(match self.get_font() {
238                    Font::Font5x8 => 3,
239                    Font::Font5x11 => 4,
240                })
241                .unwrap(),
242        );
243
244        let mut graph = CGRAMGraph::default();
245
246        graph
247            .upper
248            .iter_mut()
249            .for_each(|line| *line = self.read_u8_from_cur());
250
251        graph.lower = Some([0u8; 3]);
252
253        graph
254            .lower
255            .as_mut()
256            .unwrap()
257            .iter_mut()
258            .for_each(|line| *line = self.read_u8_from_cur());
259
260        graph
261    }
262}
263
264/// The style of the offset display window
265pub enum MoveStyle {
266    /// Always move to left
267    ForceMoveLeft,
268    /// Always move to right
269    ForceMoveRight,
270    /// Top left of display window won't cross display boundary
271    NoCrossBoundary,
272    /// Automatic find the shortest path
273    Shortest,
274}
275
276/// The flip style of split flap display
277pub enum FlipStyle {
278    /// Flip first character to target character, then flip next one
279    Sequential,
280    /// Flip all characters at once, automatically stop when the characters reach the target one
281    Simultaneous,
282}
283
284/// Show animation on LCD1602
285pub trait Anim: Ext {
286    /// Make the entire screen blink
287    ///
288    /// # Arguments
289    ///
290    /// * `count` - the number of times to blink the screen. If the value is `0`, the screen will blink endless.
291    /// * `interval_us` - The interval (in microseconds) at which the screen state changes
292    fn full_display_blink(&mut self, count: u32, interval_us: u32) {
293        match count == 0 {
294            true => loop {
295                self.delay_us(interval_us);
296                self.toggle_display();
297            },
298            false => {
299                (0..count * 2).for_each(|_| {
300                    self.delay_us(interval_us);
301                    self.toggle_display();
302                });
303            }
304        }
305    }
306
307    /// Typewriter-style display
308    ///
309    /// # Arguments
310    ///
311    /// * `str` - string to display
312    /// * `delay_us` - The interval (in microseconds) of each character show up
313    fn typewriter_write(&mut self, str: &str, delay_us: u32) {
314        str.chars().for_each(|char| {
315            self.delay_us(delay_us);
316            self.write_char_to_cur(char);
317        })
318    }
319
320    /// Split-Flap-style display
321    ///
322    /// # Arguments
323    ///
324    /// * `str` - string to display
325    /// * `fs` - flip style, see [FlipStyle]
326    /// * `max_flip_cnt` - The maximum number of times to flip the display before reaching the target character
327    /// * `per_flip_delay_us` - The delay (in microseconds) between each flip. It is recommended to set this value to at least `100_000`.
328    /// * `per_char_flip_delay_us` - Used in [FlipStyle::Sequential] mode, this is the time (in microseconds) to wait between flipping each character
329    fn split_flap_write(
330        &mut self,
331        str: &str,
332        fs: FlipStyle,
333        max_flip_cnt: Option<u8>,
334        per_flip_delay_us: u32,
335        per_char_flip_delay_us: Option<u32>,
336    ) {
337        // Checking if all characters are suitable for split flap effect (should in ASCII 0x20 to 0x7D)
338        let test_result = str
339            .chars()
340            .all(|char| char.is_ascii() && (0x20 <= char as u8) && (char as u8 <= 0x7D));
341
342        assert!(test_result, "Currently only support ASCII 0x20 to 0x7D");
343
344        let mut cursor_state_changed = false;
345
346        // turn off cursor, since it will always shift to next position
347        if self.get_cursor_state() != State::Off {
348            self.set_cursor_state(State::Off);
349            cursor_state_changed = true;
350        }
351
352        match fs {
353            FlipStyle::Sequential => {
354                assert!(
355                    per_char_flip_delay_us.is_some(),
356                    "Should set some per char delay in Sequential Mode"
357                );
358                str.chars().for_each(|char| {
359                    let cur_byte = char as u8;
360
361                    let flap_start_byte = match max_flip_cnt {
362                        None => 0x20,
363                        Some(max_flip_cnt) => {
364                            if cur_byte - max_flip_cnt < 0x20 {
365                                0x20
366                            } else {
367                                cur_byte - max_flip_cnt
368                            }
369                        }
370                    };
371
372                    let cur_pos = self.get_cursor_pos();
373
374                    self.delay_us(per_char_flip_delay_us.unwrap());
375                    (flap_start_byte..=cur_byte).for_each(|byte| {
376                        self.delay_us(per_flip_delay_us);
377                        self.write_byte_to_pos(byte, cur_pos);
378                    });
379                })
380            }
381            FlipStyle::Simultaneous => {
382                let min_char_byte = str.chars().min().unwrap() as u8;
383                let max_char_byte = str.chars().max().unwrap() as u8;
384                let str_len = str.chars().count();
385
386                let flap_start_byte = match max_flip_cnt {
387                    None => 0x20,
388                    Some(max_flip_cnt) => {
389                        if max_char_byte - min_char_byte > max_flip_cnt {
390                            min_char_byte
391                        } else if max_char_byte - max_flip_cnt < 0x20 {
392                            0x20
393                        } else {
394                            max_char_byte - max_flip_cnt
395                        }
396                    }
397                };
398
399                let start_pos = self.get_cursor_pos();
400
401                (flap_start_byte..=max_char_byte).for_each(|cur_byte| {
402                    self.delay_us(per_flip_delay_us);
403
404                    str.char_indices()
405                        .filter(|&(_, target_char)| cur_byte <= target_char as u8) // filter character that still need to flip
406                        .for_each(|(index, _)| {
407                            let cur_pos = match self.get_direction() {
408                                MoveDirection::RightToLeft => {
409                                    self.calculate_pos_by_offset(start_pos, (-(index as i8), 0))
410                                }
411                                MoveDirection::LeftToRight => {
412                                    self.calculate_pos_by_offset(start_pos, (index as i8, 0))
413                                }
414                            };
415                            self.write_byte_to_pos(cur_byte, cur_pos);
416                        });
417                });
418
419                // after the flip finished, we cannot ensure cursor position (since .filter() method)
420                // move cursor to string end
421                let end_pos = match self.get_direction() {
422                    MoveDirection::RightToLeft => {
423                        self.calculate_pos_by_offset(start_pos, (-((str_len) as i8), 0))
424                    }
425                    MoveDirection::LeftToRight => {
426                        self.calculate_pos_by_offset(start_pos, ((str_len as i8), 0))
427                    }
428                };
429                self.set_cursor_pos(end_pos);
430            }
431        }
432
433        // remember to restore cursor state
434        if cursor_state_changed {
435            self.set_cursor_state(State::On);
436        }
437    }
438
439    /// Move the display window to the specified position (measured from the upper-left corner of the display)
440    ///
441    /// # Arguments
442    ///
443    /// * `target_pos` - The target position of the display window
444    /// * `ms` - The style of movement, see [MoveStyle]
445    /// * `display_state_when_shift` - Whether to turn off the screen during the move
446    /// * `delay_us_per_step` - The delay (in microseconds) between each step of the move
447    fn shift_display_to_pos(
448        &mut self,
449        target_pos: u8,
450        ms: MoveStyle,
451        display_state_when_shift: State,
452        delay_us_per_step: u32,
453    ) {
454        let before_pos = self.get_display_offset();
455
456        // if target position is current position, just return
457        if before_pos == target_pos {
458            return;
459        }
460
461        let line_capacity = self.get_line_capacity();
462
463        let before_state = self.get_display_state();
464
465        self.set_display_state(display_state_when_shift);
466
467        // calculate offset distance
468        let (distance, direction) = match ms {
469            MoveStyle::ForceMoveLeft => {
470                if target_pos < before_pos {
471                    (before_pos - target_pos, MoveDirection::RightToLeft)
472                } else {
473                    (
474                        line_capacity - (target_pos - before_pos),
475                        MoveDirection::RightToLeft,
476                    )
477                }
478            }
479
480            MoveStyle::ForceMoveRight => {
481                if target_pos > before_pos {
482                    (target_pos - before_pos, MoveDirection::LeftToRight)
483                } else {
484                    (
485                        line_capacity - (before_pos - target_pos),
486                        MoveDirection::LeftToRight,
487                    )
488                }
489            }
490
491            MoveStyle::NoCrossBoundary => {
492                if target_pos > before_pos {
493                    (target_pos - before_pos, MoveDirection::LeftToRight)
494                } else {
495                    (before_pos - target_pos, MoveDirection::RightToLeft)
496                }
497            }
498
499            MoveStyle::Shortest => {
500                if target_pos > before_pos {
501                    if target_pos - before_pos <= line_capacity / 2 {
502                        (target_pos - before_pos, MoveDirection::LeftToRight)
503                    } else {
504                        (
505                            line_capacity - (target_pos - before_pos),
506                            MoveDirection::RightToLeft,
507                        )
508                    }
509                } else {
510                    #[allow(clippy::collapsible_else_if)]
511                    if before_pos - target_pos <= line_capacity / 2 {
512                        (before_pos - target_pos, MoveDirection::RightToLeft)
513                    } else {
514                        (
515                            line_capacity - (before_pos - target_pos),
516                            MoveDirection::LeftToRight,
517                        )
518                    }
519                }
520            }
521        };
522
523        (0..(distance)).for_each(|_| {
524            self.delay_us(delay_us_per_step);
525            self.shift_cursor_or_display(ShiftType::CursorAndDisplay, direction);
526        });
527
528        // restore original display state
529        self.set_display_state(before_state);
530    }
531}