adafruit_lcd_backpack/
lib.rs

1//! Rust driver for the [Adafruit I2C LCD backpack](https://www.adafruit.com/product/292) with MCP23008 GPIO expander
2//!
3//! _NOTE: This library is not made by Adafruit, and is not supported by them. The use of the Adafruit name
4//! is for compatibility identification purposes only._
5//!
6//! ## Overview
7//! This crate provides a driver for the Adafruit I2C LCD backpack with MCP23008 GPIO expander. It is designed to be used with the
8//! [embedded-hal](https://docs.rs/embedded-hal/latest/embedded_hal/index.html) traits for embeded systems. It supports standard
9//! HD44780 based LCD displays.
10//!
11//! ## Usage
12//! To create a new LCD backpack, use the `new` method. This will return a new LCD backpack object. Pass it the type of LCD display you
13//! are using, the I2C bus, and the delay object. Both the I2C Bus and Delay objects must implement the relevant embedded-hal traits.
14//!
15//! ```rust
16//! // The embedded-hal traits are used to define the I2C bus and delay objects
17//! use embedded_hal::{
18//!     blocking::delay::{DelayMs, DelayUs},
19//!     blocking::i2c::{Write, WriteRead},
20//! };
21//! use lcd_backpack::{LcdBackpack, LcdDisplayType};
22//!
23//! // create the I2C bus per your platform
24//! let i2c = ...;
25//!
26//! // create the delay object per your platform
27//! let delay = ...;
28//!
29//! // create the LCD backpack
30//! let mut lcd = LcdBackpack::new(LcdDisplayType::Lcd16x2, i2c, delay);
31//!
32//! // initialize the LCD
33//! if let Err(_e) = lcd.init() {
34//!    panic!("Error initializing LCD");
35//! }
36//! ```
37//! This library supports the `core::fmt::Write` trait, allowing it to be used with the `write!` macro. For example:
38//! ```rust
39//! use core::fmt::Write;
40//!
41//! // write a string to the LCD
42//! if let Err(_e) = write!(lcd, "Hello, world!") {
43//!   panic!("Error writing to LCD");
44//! }
45//! ```
46//! The various methods for controlling the LCD are also available. Each returns a `Result` that wraps the LCD backpack object. This
47//! allows you to chain the methods together. For example:
48//!
49//! ```rust
50//! // clear the display and home the cursor before writing a string
51//! if let Err(_e) = write!(lcd.clear()?.home()?, "Hello, world!") {
52//!  panic!("Error writing to LCD");
53//! }
54//! ```
55
56#![no_std]
57#![allow(dead_code, non_camel_case_types, non_upper_case_globals)]
58use embedded_hal::{
59    blocking::delay::{DelayMs, DelayUs},
60    blocking::i2c::{Write, WriteRead},
61};
62use mcp230xx::{Direction, Level, Mcp23008, Mcp230xx, Register};
63
64const RS_PIN: Mcp23008 = Mcp23008::P1;
65const ENABLE_PIN: Mcp23008 = Mcp23008::P2;
66const DATA_D4_PIN: Mcp23008 = Mcp23008::P3;
67const DATA_D5_PIN: Mcp23008 = Mcp23008::P4;
68const DATA_D6_PIN: Mcp23008 = Mcp23008::P5;
69const DATA_D7_PIN: Mcp23008 = Mcp23008::P6;
70const BACKLIGHT_PIN: Mcp23008 = Mcp23008::P7;
71
72// data pins are in order from least significant bit to most significant bit
73const DATA_PINS: [Mcp23008; 4] = [DATA_D4_PIN, DATA_D5_PIN, DATA_D6_PIN, DATA_D7_PIN];
74
75// commands
76const LCD_CMD_CLEARDISPLAY: u8 = 0x01; //  Clear display, set cursor position to zero
77const LCD_CMD_RETURNHOME: u8 = 0x02; //  Set cursor position to zero
78const LCD_CMD_ENTRYMODESET: u8 = 0x04; //  Sets the entry mode
79const LCD_CMD_DISPLAYCONTROL: u8 = 0x08; //  Controls the display; does stuff like turning it off and on
80const LCD_CMD_CURSORSHIFT: u8 = 0x10; //  Lets you move the cursor
81const LCD_CMD_FUNCTIONSET: u8 = 0x20; //  Used to send the function to set to the display
82const LCD_CMD_SETCGRAMADDR: u8 = 0x40; //  Used to set the CGRAM (character generator RAM) with characters
83const LCD_CMD_SETDDRAMADDR: u8 = 0x80; //  Used to set the DDRAM (Display Data RAM)
84
85// flags for display entry mode
86const LCD_FLAG_ENTRYRIGHT: u8 = 0x00; //  Used to set text to flow from right to left
87const LCD_FLAG_ENTRYLEFT: u8 = 0x02; //  Uset to set text to flow from left to right
88const LCD_FLAG_ENTRYSHIFTINCREMENT: u8 = 0x01; //  Used to 'right justify' text from the cursor
89const LCD_FLAG_ENTRYSHIFTDECREMENT: u8 = 0x00; //  Used to 'left justify' text from the cursor
90
91// flags for display on/off control
92const LCD_FLAG_DISPLAYON: u8 = 0x04; //  Turns the display on
93const LCD_FLAG_DISPLAYOFF: u8 = 0x00; //  Turns the display off
94const LCD_FLAG_CURSORON: u8 = 0x02; //  Turns the cursor on
95const LCD_FLAG_CURSOROFF: u8 = 0x00; //  Turns the cursor off
96const LCD_FLAG_BLINKON: u8 = 0x01; //  Turns on the blinking cursor
97const LCD_FLAG_BLINKOFF: u8 = 0x00; //  Turns off the blinking cursor
98
99// flags for display/cursor shift
100const LCD_FLAG_DISPLAYMOVE: u8 = 0x08; //  Flag for moving the display
101const LCD_FLAG_CURSORMOVE: u8 = 0x00; //  Flag for moving the cursor
102const LCD_FLAG_MOVERIGHT: u8 = 0x04; //  Flag for moving right
103const LCD_FLAG_MOVELEFT: u8 = 0x00; //  Flag for moving left
104
105// flags for function set
106const LCD_FLAG_8BITMODE: u8 = 0x10; //  LCD 8 bit mode
107const LCD_FLAG_4BITMODE: u8 = 0x00; //  LCD 4 bit mode
108const LCD_FLAG_2LINE: u8 = 0x08; //  LCD 2 line mode
109const LCD_FLAG_1LINE: u8 = 0x00; //  LCD 1 line mode
110const LCD_FLAG_5x10_DOTS: u8 = 0x04; //  10 pixel high font mode
111const LCD_FLAG_5x8_DOTS: u8 = 0x00; //  8 pixel high font mode
112
113/// The type of LCD display. This is used to determine the number of rows and columns, and the row offsets.
114pub enum LcdDisplayType {
115    /// 20x4 display
116    Lcd20x4,
117    /// 20x2 display
118    Lcd20x2,
119    /// 16x2 display
120    Lcd16x2,
121}
122
123impl LcdDisplayType {
124    /// Get the number of rows for the display type
125    const fn rows(&self) -> u8 {
126        match self {
127            LcdDisplayType::Lcd20x4 => 4,
128            LcdDisplayType::Lcd20x2 => 2,
129            LcdDisplayType::Lcd16x2 => 2,
130        }
131    }
132
133    /// Get the number of columns for the display type
134    const fn cols(&self) -> u8 {
135        match self {
136            LcdDisplayType::Lcd20x4 => 20,
137            LcdDisplayType::Lcd20x2 => 20,
138            LcdDisplayType::Lcd16x2 => 16,
139        }
140    }
141
142    /// Get the row offsets for the display type. This always returns an array of length 4.
143    /// For displays with less than 4 rows, the unused rows will be set to offsets offscreen.
144    const fn row_offsets(&self) -> [u8; 4] {
145        match self {
146            LcdDisplayType::Lcd20x4 => [0x00, 0x40, 0x14, 0x54],
147            LcdDisplayType::Lcd20x2 => [0x00, 0x40, 0x00, 0x40],
148            LcdDisplayType::Lcd16x2 => [0x00, 0x40, 0x10, 0x50],
149        }
150    }
151}
152
153pub struct LcdBackpack<I2C, D> {
154    register: Mcp230xx<I2C, Mcp23008>,
155    delay: D,
156    lcd_type: LcdDisplayType,
157    display_function: u8,
158    display_control: u8,
159    display_mode: u8,
160}
161
162/// Errors that can occur when using the LCD backpack
163pub enum Error<I2C_ERR> {
164    /// I2C error returned from the underlying I2C implementation
165    I2cError(I2C_ERR),
166    /// The MCP23008 interrupt pin is not found
167    InterruptPinError,
168    /// Row is out of range
169    RowOutOfRange,
170    /// Column is out of range
171    ColumnOutOfRange,
172    /// Formatting error
173    #[cfg(feature = "defmt")]
174    FormattingError,
175}
176
177impl<I2C_ERR> From<I2C_ERR> for Error<I2C_ERR> {
178    fn from(err: I2C_ERR) -> Self {
179        Error::I2cError(err)
180    }
181}
182
183impl<I2C_ERR> From<mcp230xx::Error<I2C_ERR>> for Error<I2C_ERR> {
184    fn from(err: mcp230xx::Error<I2C_ERR>) -> Self {
185        match err {
186            mcp230xx::Error::BusError(e) => Error::I2cError(e),
187            mcp230xx::Error::InterruptPinError => Error::InterruptPinError,
188        }
189    }
190}
191
192#[cfg(feature = "defmt")]
193impl<I2C_ERR> defmt::Format for Error<I2C_ERR>
194where
195    I2C_ERR: defmt::Format,
196{
197    fn format(&self, fmt: defmt::Formatter) {
198        match self {
199            Error::I2cError(e) => defmt::write!(fmt, "I2C error: {:?}", e),
200            Error::InterruptPinError => defmt::write!(fmt, "Interrupt pin not found"),
201            Error::RowOutOfRange => defmt::write!(fmt, "Row out of range"),
202            Error::ColumnOutOfRange => defmt::write!(fmt, "Column out of range"),
203            Error::FormattingError => defmt::write!(fmt, "Formatting error"),
204        }
205    }
206}
207
208impl<I2C, I2C_ERR, D> LcdBackpack<I2C, D>
209where
210    I2C: Write<Error = I2C_ERR> + WriteRead<Error = I2C_ERR>,
211    D: DelayMs<u16> + DelayUs<u16>,
212{
213    /// Create a new LCD backpack with the default I2C address of 0x20
214    pub fn new(lcd_type: LcdDisplayType, i2c: I2C, delay: D) -> Self {
215        Self::new_with_address(lcd_type, i2c, delay, 0x20)
216    }
217
218    /// Create a new LCD backpack with the specified I2C address
219    pub fn new_with_address(lcd_type: LcdDisplayType, i2c: I2C, delay: D, address: u8) -> Self {
220        let register = match Mcp230xx::<I2C, Mcp23008>::new(i2c, address) {
221            Ok(r) => r,
222            Err(_) => panic!("Could not create MCP23008"),
223        };
224
225        Self {
226            register,
227            delay,
228            lcd_type,
229            display_function: LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE,
230            display_control: LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF,
231            display_mode: LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT,
232        }
233    }
234
235    /// Get a mutable reference to the delay object. This is useful as the delay objectis moved into the LCD backpack during initialization.
236    pub fn delay(&mut self) -> &mut D {
237        &mut self.delay
238    }
239
240    /// Initialize the LCD. Must be called before any other methods. Will turn on the blanked display, with no cursor or blinking.
241    pub fn init(&mut self) -> Result<&mut Self, Error<I2C_ERR>> {
242        // set up back light
243        self.register
244            .set_direction(BACKLIGHT_PIN, Direction::Output)?;
245        self.register.set_gpio(BACKLIGHT_PIN, Level::High)?;
246
247        // set data pins to output
248        for pin in DATA_PINS.iter() {
249            self.register.set_direction(*pin, Direction::Output)?;
250        }
251
252        // RS & Enable piun
253        self.register.set_direction(RS_PIN, Direction::Output)?;
254        self.register.set_direction(ENABLE_PIN, Direction::Output)?;
255
256        // need to wait 40ms after power rises above 2.7V before sending any commands. wait alittle longer.
257        self.delay().delay_ms(50);
258
259        // pull RS & Enable low to start command. RW is hardwired low on backpack.
260        self.register.set_gpio(RS_PIN, Level::Low)?;
261        self.register.set_gpio(ENABLE_PIN, Level::Low)?;
262
263        // Put LCD into 4 bit mode, device starts in 8 bit mode
264        self.write_4_bits(0x03)?;
265        self.delay().delay_ms(5);
266        self.write_4_bits(0x03)?;
267        self.delay().delay_ms(5);
268        self.write_4_bits(0x03)?;
269        self.delay().delay_us(150);
270        self.write_4_bits(0x02)?;
271
272        // set up the display
273        self.send_command(LCD_CMD_FUNCTIONSET | self.display_function)?;
274        self.send_command(LCD_CMD_DISPLAYCONTROL | self.display_control)?;
275        self.send_command(LCD_CMD_ENTRYMODESET | self.display_mode)?;
276        self.clear()?;
277        self.home()?;
278
279        Ok(self)
280    }
281
282    //--------------------------------------------------------------------------------------------------
283    // high level commands, for the user!
284    //--------------------------------------------------------------------------------------------------
285
286    /// Clear the display
287    pub fn clear(&mut self) -> Result<&mut Self, Error<I2C_ERR>> {
288        self.send_command(LCD_CMD_CLEARDISPLAY)?;
289        self.delay().delay_ms(2);
290        Ok(self)
291    }
292
293    /// Set the cursor to the home position
294    pub fn home(&mut self) -> Result<&mut Self, Error<I2C_ERR>> {
295        self.send_command(LCD_CMD_RETURNHOME)?;
296        self.delay().delay_ms(2);
297        Ok(self)
298    }
299
300    /// Set the cursor position at specified column and row
301    pub fn set_cursor(&mut self, col: u8, row: u8) -> Result<&mut Self, Error<I2C_ERR>> {
302        if row >= self.lcd_type.rows() {
303            return Err(Error::RowOutOfRange);
304        }
305        if col >= self.lcd_type.cols() {
306            return Err(Error::ColumnOutOfRange);
307        }
308
309        self.send_command(
310            LCD_CMD_SETDDRAMADDR | (col + self.lcd_type.row_offsets()[row as usize]),
311        )?;
312        Ok(self)
313    }
314
315    /// Set the cursor visibility
316    pub fn show_cursor(&mut self, show_cursor: bool) -> Result<&mut Self, Error<I2C_ERR>> {
317        if show_cursor {
318            self.display_control |= LCD_FLAG_CURSORON;
319        } else {
320            self.display_control &= !LCD_FLAG_CURSORON;
321        }
322        self.send_command(LCD_CMD_DISPLAYCONTROL | self.display_control)?;
323        Ok(self)
324    }
325
326    /// Set the cursor blinking
327    pub fn blink_cursor(&mut self, blink_cursor: bool) -> Result<&mut Self, Error<I2C_ERR>> {
328        if blink_cursor {
329            self.display_control |= LCD_FLAG_BLINKON;
330        } else {
331            self.display_control &= !LCD_FLAG_BLINKON;
332        }
333        self.send_command(LCD_CMD_DISPLAYCONTROL | self.display_control)?;
334        Ok(self)
335    }
336
337    /// Set the display visibility
338    pub fn show_display(&mut self, show_display: bool) -> Result<&mut Self, Error<I2C_ERR>> {
339        if show_display {
340            self.display_control |= LCD_FLAG_DISPLAYON;
341        } else {
342            self.display_control &= !LCD_FLAG_DISPLAYON;
343        }
344        self.send_command(LCD_CMD_DISPLAYCONTROL | self.display_control)?;
345        Ok(self)
346    }
347
348    /// Scroll the display to the left
349    pub fn scroll_display_left(&mut self) -> Result<&mut Self, Error<I2C_ERR>> {
350        self.send_command(LCD_CMD_CURSORSHIFT | LCD_FLAG_DISPLAYMOVE | LCD_FLAG_MOVELEFT)?;
351        Ok(self)
352    }
353
354    /// Scroll the display to the right
355    pub fn scroll_display_right(&mut self) -> Result<&mut Self, Error<I2C_ERR>> {
356        self.send_command(LCD_CMD_CURSORSHIFT | LCD_FLAG_DISPLAYMOVE | LCD_FLAG_MOVERIGHT)?;
357        Ok(self)
358    }
359
360    /// Set the text flow direction to left to right
361    pub fn left_to_right(&mut self) -> Result<&mut Self, Error<I2C_ERR>> {
362        self.display_mode |= LCD_FLAG_ENTRYLEFT;
363        self.send_command(LCD_CMD_ENTRYMODESET | self.display_mode)?;
364        Ok(self)
365    }
366
367    /// Set the text flow direction to right to left
368    pub fn right_to_left(&mut self) -> Result<&mut Self, Error<I2C_ERR>> {
369        self.display_mode &= !LCD_FLAG_ENTRYLEFT;
370        self.send_command(LCD_CMD_ENTRYMODESET | self.display_mode)?;
371        Ok(self)
372    }
373
374    /// Set the auto scroll mode
375    pub fn autoscroll(&mut self, autoscroll: bool) -> Result<&mut Self, Error<I2C_ERR>> {
376        if autoscroll {
377            self.display_mode |= LCD_FLAG_ENTRYSHIFTINCREMENT;
378        } else {
379            self.display_mode &= !LCD_FLAG_ENTRYSHIFTINCREMENT;
380        }
381        self.send_command(LCD_CMD_ENTRYMODESET | self.display_mode)?;
382        Ok(self)
383    }
384
385    /// Create a new custom character
386    pub fn create_char(
387        &mut self,
388        location: u8,
389        charmap: [u8; 8],
390    ) -> Result<&mut Self, Error<I2C_ERR>> {
391        self.send_command(LCD_CMD_SETCGRAMADDR | ((location & 0x7) << 3))?;
392        for &charmap_byte in charmap.iter() {
393            self.write_data(charmap_byte)?;
394        }
395        Ok(self)
396    }
397
398    /// Prints a string to the LCD at the current cursor position
399    pub fn print(&mut self, text: &str) -> Result<&mut Self, Error<I2C_ERR>> {
400        for c in text.chars() {
401            self.write_data(c as u8)?;
402        }
403        Ok(self)
404    }
405
406    //--------------------------------------------------------------------------------------------------
407    // Internal data writing functions
408    //--------------------------------------------------------------------------------------------------
409
410    /// Write 4 bits to the LCD
411    fn write_4_bits(&mut self, value: u8) -> Result<(), Error<I2C_ERR>> {
412        // get the current value of the register byte
413        let mut register_contents = self.register.read(Register::GPIO.into())?;
414
415        // set bit 0, data pin 4
416        for (index, pin) in DATA_PINS.iter().enumerate() {
417            let bit_mask = 1 << (*pin as u8);
418            register_contents &= !bit_mask;
419            if value & (1 << index) != 0 {
420                register_contents |= bit_mask;
421            }
422        }
423
424        // set the enable pin low in the register_contents
425        register_contents &= !(1 << (ENABLE_PIN as u8));
426
427        // write the new register contents
428        self.register
429            .write(Register::GPIO.into(), register_contents)?;
430
431        // pulse ENABLE pin quickly using the known value of the register contents
432        self.delay().delay_us(1);
433        register_contents |= 1 << (ENABLE_PIN as u8); // set enable pin high
434        self.register
435            .write(Register::GPIO.into(), register_contents)?;
436        self.delay().delay_us(1);
437        register_contents &= !(1 << (ENABLE_PIN as u8)); // set enable pin low
438        self.register
439            .write(Register::GPIO.into(), register_contents)?;
440        self.delay().delay_us(100);
441
442        Ok(())
443    }
444
445    /// Write 8 bits to the LCD using 4 bit mode
446    fn write_8_bits(&mut self, value: u8) -> Result<(), Error<I2C_ERR>> {
447        self.write_4_bits(value >> 4)?;
448        self.write_4_bits(value & 0x0F)?;
449        Ok(())
450    }
451
452    /// Send a command to the LCD
453    pub fn send_command(&mut self, command: u8) -> Result<(), Error<I2C_ERR>> {
454        self.register.set_gpio(RS_PIN, Level::Low)?;
455        self.write_8_bits(command)?;
456        Ok(())
457    }
458
459    /// Send data to the LCD
460    pub fn write_data(&mut self, value: u8) -> Result<(), Error<I2C_ERR>> {
461        self.register.set_gpio(RS_PIN, Level::High)?;
462        self.write_8_bits(value)?;
463        Ok(())
464    }
465
466    /// Pulse the enable pin
467    fn pulse_enable(&mut self) -> Result<(), Error<I2C_ERR>> {
468        self.register.set_gpio(ENABLE_PIN, Level::Low)?;
469        self.delay().delay_us(1);
470        self.register.set_gpio(ENABLE_PIN, Level::High)?;
471        self.delay().delay_us(1);
472        self.register.set_gpio(ENABLE_PIN, Level::Low)?;
473        self.delay().delay_us(100);
474
475        Ok(())
476    }
477}
478
479/// Implement the `core::fmt::Write` trait for the LCD backpack, allowing it to be used with the `write!` macro.
480impl<I2C, I2C_ERR, D> core::fmt::Write for LcdBackpack<I2C, D>
481where
482    I2C: Write<Error = I2C_ERR> + WriteRead<Error = I2C_ERR>,
483    D: DelayMs<u16> + DelayUs<u16>,
484{
485    fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
486        if let Err(_error) = self.print(s) {
487            return Err(core::fmt::Error);
488        }
489        Ok(())
490    }
491}