grove_lcd_rgb/
lib.rs

1//! Grove LCD RGB Backlight Driver
2//!
3//! A platform-agnostic driver for the Grove LCD RGB Backlight display using embedded-hal traits.
4//! This driver supports the 16x2 character LCD with RGB backlight control via I2C.
5//!
6//! # Features
7//! - Full LCD control (clear, cursor positioning, etc.)
8//! - RGB backlight color control
9//! - Custom character support
10//! - Display scrolling
11//! - Cursor and blink control
12//!
13//! # Example
14//! ```no_run
15//! use grove_lcd_rgb::GroveLcd;
16//! 
17//! let mut lcd = GroveLcd::new(i2c);
18//! lcd.begin(16, 2).unwrap();
19//! lcd.set_rgb(255, 0, 0).unwrap(); // Red backlight
20//! lcd.print("Hello, World!").unwrap();
21//! ```
22
23#![no_std]
24
25use embedded_hal::delay::DelayNs;
26use embedded_hal::i2c::I2c;
27
28/// I2C address for the LCD text controller
29pub const LCD_ADDRESS: u8 = 0x3E;
30
31/// I2C address for the RGB backlight controller
32pub const RGB_ADDRESS: u8 = 0x62;
33
34/// Alternative RGB address for v5 hardware
35pub const RGB_ADDRESS_V5: u8 = 0x30;
36
37// LCD Commands
38const LCD_CLEARDISPLAY: u8 = 0x01;
39const LCD_RETURNHOME: u8 = 0x02;
40const LCD_ENTRYMODESET: u8 = 0x04;
41const LCD_DISPLAYCONTROL: u8 = 0x08;
42const LCD_CURSORSHIFT: u8 = 0x10;
43const LCD_FUNCTIONSET: u8 = 0x20;
44const LCD_SETCGRAMADDR: u8 = 0x40;
45#[allow(dead_code)]
46const LCD_SETDDRAMADDR: u8 = 0x80;
47
48// Entry Mode flags
49#[allow(dead_code)]
50const LCD_ENTRYRIGHT: u8 = 0x00;
51const LCD_ENTRYLEFT: u8 = 0x02;
52const LCD_ENTRYSHIFTINCREMENT: u8 = 0x01;
53const LCD_ENTRYSHIFTDECREMENT: u8 = 0x00;
54
55// Display Control flags
56const LCD_DISPLAYON: u8 = 0x04;
57#[allow(dead_code)]
58const LCD_DISPLAYOFF: u8 = 0x00;
59const LCD_CURSORON: u8 = 0x02;
60const LCD_CURSOROFF: u8 = 0x00;
61const LCD_BLINKON: u8 = 0x01;
62const LCD_BLINKOFF: u8 = 0x00;
63
64// Cursor Shift flags
65const LCD_DISPLAYMOVE: u8 = 0x08;
66#[allow(dead_code)]
67const LCD_CURSORMOVE: u8 = 0x00;
68const LCD_MOVERIGHT: u8 = 0x04;
69const LCD_MOVELEFT: u8 = 0x00;
70
71// Function Set flags
72#[allow(dead_code)]
73const LCD_8BITMODE: u8 = 0x10;
74#[allow(dead_code)]
75const LCD_4BITMODE: u8 = 0x00;
76const LCD_2LINE: u8 = 0x08;
77#[allow(dead_code)]
78const LCD_1LINE: u8 = 0x00;
79const LCD_5X10_DOTS: u8 = 0x04;
80#[allow(dead_code)]
81const LCD_5X8_DOTS: u8 = 0x00;
82
83// RGB Registers
84const REG_MODE1: u8 = 0x00;
85const REG_MODE2: u8 = 0x01;
86const REG_OUTPUT: u8 = 0x08;
87const REG_RED: u8 = 0x04;
88const REG_GREEN: u8 = 0x03;
89const REG_BLUE: u8 = 0x02;
90
91/// Dot size for LCD characters
92#[derive(Debug, Clone, Copy)]
93pub enum DotSize {
94    /// 5x8 dots per character (default)
95    Dots5x8,
96    /// 5x10 dots per character (only for 1-line displays)
97    Dots5x10,
98}
99
100/// Errors that can occur when using the LCD
101#[derive(Debug, Clone, Copy)]
102pub enum LcdError<E> {
103    /// I2C communication error
104    I2c(E),
105    /// Invalid parameter provided
106    InvalidParameter,
107}
108
109impl<E: core::fmt::Debug> core::fmt::Display for LcdError<E> {
110    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
111        match self {
112            LcdError::I2c(e) => write!(f, "I2C Error: {:?}", e),
113            LcdError::InvalidParameter => write!(f, "Invalid parameter"),
114        }
115    }
116}
117
118impl<E> From<E> for LcdError<E> {
119    fn from(error: E) -> Self {
120        LcdError::I2c(error)
121    }
122}
123
124/// Grove LCD RGB Backlight driver
125pub struct GroveLcd<I2C, D> {
126    i2c: I2C,
127    delay: D,
128    display_function: u8,
129    display_control: u8,
130    display_mode: u8,
131    num_lines: u8,
132    curr_line: u8,
133    rgb_addr: u8,
134}
135
136impl<I2C, D, E> GroveLcd<I2C, D>
137where
138    I2C: I2c<Error = E>,
139    D: DelayNs,
140{
141    /// Create a new Grove LCD instance
142    pub fn new(i2c: I2C, delay: D) -> Self {
143        Self {
144            i2c,
145            delay,
146            display_function: 0,
147            display_control: 0,
148            display_mode: 0,
149            num_lines: 0,
150            curr_line: 0,
151            rgb_addr: RGB_ADDRESS,
152        }
153    }
154
155    /// Initialize the LCD with the specified columns and rows
156    ///
157    /// # Arguments
158    /// * `cols` - Number of columns (typically 16)
159    /// * `rows` - Number of rows (typically 2)
160    pub fn begin(&mut self, cols: u8, rows: u8) -> Result<(), LcdError<E>> {
161        self.begin_with_dotsize(cols, rows, DotSize::Dots5x8)
162    }
163
164    /// Initialize the LCD with specified columns, rows, and dot size
165    pub fn begin_with_dotsize(
166        &mut self,
167        _cols: u8,
168        lines: u8,
169        dotsize: DotSize,
170    ) -> Result<(), LcdError<E>> {
171        if lines > 1 {
172            self.display_function |= LCD_2LINE;
173        }
174        self.num_lines = lines;
175        self.curr_line = 0;
176
177        // For some 1-line displays you can select a 10-pixel high font
178        if matches!(dotsize, DotSize::Dots5x10) && lines == 1 {
179            self.display_function |= LCD_5X10_DOTS;
180        }
181
182        // Wait for LCD to power up - increased for reliability
183        self.delay.delay_ms(100);
184
185        // Initialize the display following HD44780 datasheet procedure
186        // Retry first command for more reliable initialization
187        let mut last_err = None;
188        for _attempt in 0..3 {
189            match self.command(LCD_FUNCTIONSET | self.display_function) {
190                Ok(_) => {
191                    last_err = None;
192                    break;
193                }
194                Err(e) => {
195                    last_err = Some(e);
196                    self.delay.delay_ms(10);
197                }
198            }
199        }
200        if let Some(e) = last_err {
201            return Err(e);
202        }
203        
204        self.delay.delay_us(4500);
205
206        self.command(LCD_FUNCTIONSET | self.display_function)?;
207        self.delay.delay_us(150);
208
209        self.command(LCD_FUNCTIONSET | self.display_function)?;
210        self.command(LCD_FUNCTIONSET | self.display_function)?;
211
212        // Turn display on with no cursor or blinking
213        self.display_control = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF;
214        self.display()?;
215
216        // Clear display
217        self.clear()?;
218
219        // Set text direction (left to right)
220        self.display_mode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT;
221        self.command(LCD_ENTRYMODESET | self.display_mode)?;
222
223        // Detect and initialize RGB backlight chip with retries
224        let mut rgb_v5_detected = false;
225        for _attempt in 0..3 {
226            self.delay.delay_ms(10);
227            // Try v5 address first
228            if self.i2c.write(RGB_ADDRESS_V5, &[]).is_ok() {
229                rgb_v5_detected = true;
230                break;
231            }
232        }
233        
234        if rgb_v5_detected {
235            self.rgb_addr = RGB_ADDRESS_V5;
236            self.set_reg(0x00, 0x07).ok(); // Reset - ignore errors
237            self.delay.delay_ms(200);
238            self.set_reg(0x04, 0x15).ok(); // Set all LEDs always on
239        } else {
240            // Try v4 address
241            let mut rgb_v4_detected = false;
242            for _attempt in 0..3 {
243                self.delay.delay_ms(10);
244                if self.i2c.write(RGB_ADDRESS, &[]).is_ok() {
245                    rgb_v4_detected = true;
246                    break;
247                }
248            }
249            
250            if rgb_v4_detected {
251                self.rgb_addr = RGB_ADDRESS;
252                self.set_reg(REG_MODE1, 0).ok();
253                self.set_reg(REG_OUTPUT, 0xFF).ok();
254                self.set_reg(REG_MODE2, 0x20).ok();
255            }
256        }
257
258        // Set default white backlight (ignore errors if RGB not available)
259        let _ = self.set_rgb(255, 255, 255);
260
261        Ok(())
262    }
263
264    /// Clear the display
265    pub fn clear(&mut self) -> Result<(), LcdError<E>> {
266        self.command(LCD_CLEARDISPLAY)?;
267        self.delay.delay_ms(2);
268        Ok(())
269    }
270
271    /// Return cursor to home position (0, 0)
272    pub fn home(&mut self) -> Result<(), LcdError<E>> {
273        self.command(LCD_RETURNHOME)?;
274        self.delay.delay_ms(2);
275        Ok(())
276    }
277
278    /// Set cursor position
279    ///
280    /// # Arguments
281    /// * `col` - Column (0-15 for 16-column display)
282    /// * `row` - Row (0-1 for 2-row display)
283    pub fn set_cursor(&mut self, col: u8, row: u8) -> Result<(), LcdError<E>> {
284        let row = row.min(self.num_lines.saturating_sub(1));
285        let val = if row == 0 {
286            col | 0x80
287        } else {
288            col | 0xc0
289        };
290        
291        let data = [0x80, val];
292        self.i2c.write(LCD_ADDRESS, &data)?;
293        Ok(())
294    }
295
296    /// Turn display off (data remains in memory)
297    pub fn no_display(&mut self) -> Result<(), LcdError<E>> {
298        self.display_control &= !LCD_DISPLAYON;
299        self.command(LCD_DISPLAYCONTROL | self.display_control)
300    }
301
302    /// Turn display on
303    pub fn display(&mut self) -> Result<(), LcdError<E>> {
304        self.display_control |= LCD_DISPLAYON;
305        self.command(LCD_DISPLAYCONTROL | self.display_control)
306    }
307
308    /// Turn cursor off
309    pub fn no_cursor(&mut self) -> Result<(), LcdError<E>> {
310        self.display_control &= !LCD_CURSORON;
311        self.command(LCD_DISPLAYCONTROL | self.display_control)
312    }
313
314    /// Turn cursor on
315    pub fn cursor(&mut self) -> Result<(), LcdError<E>> {
316        self.display_control |= LCD_CURSORON;
317        self.command(LCD_DISPLAYCONTROL | self.display_control)
318    }
319
320    /// Turn cursor blinking off
321    pub fn no_blink(&mut self) -> Result<(), LcdError<E>> {
322        self.display_control &= !LCD_BLINKON;
323        self.command(LCD_DISPLAYCONTROL | self.display_control)
324    }
325
326    /// Turn cursor blinking on
327    pub fn blink(&mut self) -> Result<(), LcdError<E>> {
328        self.display_control |= LCD_BLINKON;
329        self.command(LCD_DISPLAYCONTROL | self.display_control)
330    }
331
332    /// Scroll display left without changing RAM
333    pub fn scroll_display_left(&mut self) -> Result<(), LcdError<E>> {
334        self.command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVELEFT)
335    }
336
337    /// Scroll display right without changing RAM
338    pub fn scroll_display_right(&mut self) -> Result<(), LcdError<E>> {
339        self.command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVERIGHT)
340    }
341
342    /// Set text flow direction left to right
343    pub fn left_to_right(&mut self) -> Result<(), LcdError<E>> {
344        self.display_mode |= LCD_ENTRYLEFT;
345        self.command(LCD_ENTRYMODESET | self.display_mode)
346    }
347
348    /// Set text flow direction right to left
349    pub fn right_to_left(&mut self) -> Result<(), LcdError<E>> {
350        self.display_mode &= !LCD_ENTRYLEFT;
351        self.command(LCD_ENTRYMODESET | self.display_mode)
352    }
353
354    /// Enable autoscroll (display shifts with each character)
355    pub fn autoscroll(&mut self) -> Result<(), LcdError<E>> {
356        self.display_mode |= LCD_ENTRYSHIFTINCREMENT;
357        self.command(LCD_ENTRYMODESET | self.display_mode)
358    }
359
360    /// Disable autoscroll
361    pub fn no_autoscroll(&mut self) -> Result<(), LcdError<E>> {
362        self.display_mode &= !LCD_ENTRYSHIFTINCREMENT;
363        self.command(LCD_ENTRYMODESET | self.display_mode)
364    }
365
366    /// Create a custom character
367    ///
368    /// # Arguments
369    /// * `location` - Character location (0-7)
370    /// * `charmap` - 8-byte array defining the character bitmap
371    pub fn create_char(&mut self, location: u8, charmap: &[u8; 8]) -> Result<(), LcdError<E>> {
372        let location = location & 0x7;
373        self.command(LCD_SETCGRAMADDR | (location << 3))?;
374        
375        for &byte in charmap {
376            self.write_data(byte)?;
377        }
378        
379        Ok(())
380    }
381
382    /// Set RGB backlight color
383    ///
384    /// # Arguments
385    /// * `r` - Red component (0-255)
386    /// * `g` - Green component (0-255)
387    /// * `b` - Blue component (0-255)
388    pub fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> Result<(), LcdError<E>> {
389        // v5 uses different register addresses than v4
390        if self.rgb_addr == RGB_ADDRESS_V5 {
391            self.set_reg(0x06, r)?;  // v5 red register
392            self.set_reg(0x07, g)?;  // v5 green register
393            self.set_reg(0x08, b)?;  // v5 blue register
394        } else {
395            self.set_reg(REG_RED, r)?;    // v4: 0x04
396            self.set_reg(REG_GREEN, g)?;  // v4: 0x03
397            self.set_reg(REG_BLUE, b)?;   // v4: 0x02
398        }
399        Ok(())
400    }
401
402    /// Turn off backlight
403    pub fn backlight_off(&mut self) -> Result<(), LcdError<E>> {
404        self.set_rgb(0, 0, 0)
405    }
406
407    /// Set backlight to white
408    pub fn backlight_white(&mut self) -> Result<(), LcdError<E>> {
409        self.set_rgb(255, 255, 255)
410    }
411
412    /// Print a string to the LCD
413    pub fn print(&mut self, s: &str) -> Result<(), LcdError<E>> {
414        for c in s.chars() {
415            self.write_data(c as u8)?;
416        }
417        Ok(())
418    }
419
420    /// Print a single byte to the LCD
421    pub fn write(&mut self, value: u8) -> Result<(), LcdError<E>> {
422        self.write_data(value)
423    }
424
425    // Private helper methods
426
427    fn command(&mut self, value: u8) -> Result<(), LcdError<E>> {
428        let data = [0x80, value];
429        self.i2c.write(LCD_ADDRESS, &data)?;
430        Ok(())
431    }
432
433    fn write_data(&mut self, value: u8) -> Result<(), LcdError<E>> {
434        let data = [0x40, value];
435        self.i2c.write(LCD_ADDRESS, &data)?;
436        Ok(())
437    }
438
439    fn set_reg(&mut self, reg: u8, value: u8) -> Result<(), LcdError<E>> {
440        let data = [reg, value];
441        self.i2c.write(self.rgb_addr, &data)?;
442        Ok(())
443    }
444
445    /// Consume the driver and return the I2C peripheral
446    pub fn release(self) -> (I2C, D) {
447        (self.i2c, self.delay)
448    }
449}