From 33a52a1f1332daa9c7b2fffe67ecaeb645b68e45 Mon Sep 17 00:00:00 2001 From: Michael Kamprath Date: Sat, 19 Oct 2024 14:42:59 -0700 Subject: [PATCH] added support for dual hd44780 displays --- CHANGELOG.md | 13 + Cargo.toml | 2 +- README.md | 3 + src/adapter_config.rs | 268 ++------- src/adapter_config/adafruit_lcd_backpack.rs | 141 +++++ src/adapter_config/dual_hd44780.rs | 146 +++++ src/adapter_config/generic_pcf8574t.rs | 138 +++++ src/lib.rs | 579 +++++++++++++++----- 8 files changed, 928 insertions(+), 362 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/adapter_config/adafruit_lcd_backpack.rs create mode 100644 src/adapter_config/dual_hd44780.rs create mode 100644 src/adapter_config/generic_pcf8574t.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0d27a58 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# `i2c-character-display` Change Log + +## [Unreleased] + +## [0.2.0] - 2024-10-19 +* Added support for 40x4 character displays using two HD44780 controllers with a PCF8574T I2C adapter wired with two enable pins, one for each controller. +* Improved unit tests + +## 0.1.0 +Initial release. Support for both Generic PCF8574T I2C and Adafruit Backpack character display adapters. + +[Unreleased]: https://github.com/michaelkamprath/bespokeasm/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/michaelkamprath/bespokeasm/compare/v0.1.0...v0.2.0 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 8a4c689..36e010e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "i2c-character-display" -version = "0.1.0" +version = "0.2.0" edition = "2021" description = "Driver for HD44780-based character displays connected via a I2C adapter" license = "MIT" diff --git a/README.md b/README.md index 44a5c3f..84c2cbb 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Key features include: - Backlight control - `core::fmt::Write` implementation for easy use with the `write!` macro - Compatible with the `embedded-hal` traits v1.0 and later +- Support for character displays that used multiple HD44780 drivers, such as the 40x4 display ## Usage Add this to your `Cargo.toml`: @@ -43,6 +44,8 @@ let delay = ...; // DelayMs implementation let mut lcd = AdafruitLCDBackpack::new(i2c, LcdDisplayType::Lcd16x2, delay); // PCF8574T adapter let mut lcd = CharacterDisplayPCF8574T::new(i2c, LcdDisplayType::Lcd16x2, delay); +// Character display with dual HD44780 drivers using a PCF8574T I2C adapter +let mut lcd = CharacterDisplayDualHD44780::new(i2c, LcdDisplayType::Lcd40x4, delay); ``` When creating the display object, you can choose the display type from the `LcdDisplayType` enum. The display type should match the physical display you are using. This display type configures the number of rows and columns, and the internal row offsets for the display. diff --git a/src/adapter_config.rs b/src/adapter_config.rs index 6255f7d..13df21d 100644 --- a/src/adapter_config.rs +++ b/src/adapter_config.rs @@ -1,260 +1,60 @@ -use bitfield::bitfield; -use core::marker::PhantomData; -use embedded_hal::i2c; - -pub trait AdapterConfigTrait: Default -where - I2C: i2c::I2c, -{ - fn bits(&self) -> u8; - fn default_i2c_address() -> u8; - - fn set_rs(&mut self, value: u8); - fn set_rw(&mut self, value: u8); - fn set_enable(&mut self, value: u8); - fn set_backlight(&mut self, value: u8); - fn set_data(&mut self, value: u8); +pub mod adafruit_lcd_backpack; +pub mod dual_hd44780; +pub mod generic_pcf8574t; - fn init(&self, _i2c: &mut I2C, _i2c_address: u8) -> Result<(), I2C::Error> { - Ok(()) - } - - fn write_bits_to_gpio(&self, i2c: &mut I2C, i2c_address: u8) -> Result<(), I2C::Error> { - let data = [self.bits()]; - i2c.write(i2c_address, &data)?; - Ok(()) - } -} - -// Configuration for the PCF8574T based 4-bit LCD interface sold -bitfield! { - pub struct GenericPCF8574TBitField(u8); - impl Debug; - impl BitAnd; - pub rs, set_rs: 0, 0; - pub rw, set_rw: 1, 1; - pub enable, set_enable: 2, 2; - pub backlight, set_backlight: 3, 3; - pub data, set_data: 7, 4; -} +use crate::LcdDisplayType; +use embedded_hal::i2c; -pub struct GenericPCF8574TConfig { - bits: GenericPCF8574TBitField, - _marker: PhantomData, +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum AdapterError { + /// The device ID was not recognized + BadDeviceId, } -impl Default for GenericPCF8574TConfig -where - I2C: i2c::I2c, -{ - fn default() -> Self { - Self { - bits: GenericPCF8574TBitField(0), - _marker: PhantomData, +#[cfg(feature = "defmt")] +impl defmt::Format for AdapterError { + fn format(&self, fmt: defmt::Formatter) { + match self { + AdapterError::BadDeviceId => defmt::write!(fmt, "BadDeviceId"), } } } -impl AdapterConfigTrait for GenericPCF8574TConfig -where - I2C: i2c::I2c, -{ - fn bits(&self) -> u8 { - self.bits.0 - } - - fn default_i2c_address() -> u8 { - 0x27 - } - - fn set_rs(&mut self, value: u8) { - self.bits.set_rs(value); - } - - fn set_rw(&mut self, value: u8) { - self.bits.set_rw(value); - } - - fn set_enable(&mut self, value: u8) { - self.bits.set_enable(value); - } - - fn set_backlight(&mut self, value: u8) { - self.bits.set_backlight(value); - } - - fn set_data(&mut self, value: u8) { - self.bits.set_data(value); - } -} - -// Configuration for the MCP23008 based LCD backpack from Adafruit -bitfield! { - pub struct AdafruitLCDBackpackBitField(u8); - impl Debug; - impl BitAnd; - pub rs, set_rs: 1, 1; - pub enable, set_enable: 2, 2; - pub backlight, set_backlight: 7, 7; - pub data, set_data: 6, 3; -} - -pub struct AdafruitLCDBackpackConfig { - bits: AdafruitLCDBackpackBitField, - _marker: PhantomData, -} - -impl Default for AdafruitLCDBackpackConfig -where - I2C: i2c::I2c, -{ - fn default() -> Self { - Self { - bits: AdafruitLCDBackpackBitField(0), - _marker: PhantomData, - } - } -} -/// Configuration for the MCP23008 based LCD backpack from Adafruit -impl AdapterConfigTrait for AdafruitLCDBackpackConfig +pub trait AdapterConfigTrait: Default where I2C: i2c::I2c, { - fn bits(&self) -> u8 { - self.bits.0 - } - - fn default_i2c_address() -> u8 { - 0x20 - } - - fn set_rs(&mut self, value: u8) { - self.bits.set_rs(value); - } - - /// Adafruit LCD Backpack doesn't use RW - fn set_rw(&mut self, _value: u8) { - // Not used - } - - fn set_enable(&mut self, value: u8) { - self.bits.set_enable(value); - } - - fn set_backlight(&mut self, value: u8) { - self.bits.set_backlight(value); - } + fn bits(&self) -> u8; + fn default_i2c_address() -> u8; - fn set_data(&mut self, value: u8) { - self.bits.set_data(value); - } + fn set_rs(&mut self, value: bool); + fn set_rw(&mut self, value: bool); + /// Sets the enable pin for the given device. Most displays only have one enable pin, so the device + /// parameter is ignored. For displays with two enable pins, the device parameter is used to determine + /// which enable pin to set. + fn set_enable(&mut self, value: bool, device: usize) -> Result<(), AdapterError>; + fn set_backlight(&mut self, value: bool); + fn set_data(&mut self, value: u8); - fn init(&self, i2c: &mut I2C, i2c_address: u8) -> Result<(), I2C::Error> { - // Set the MCP23008 IODIR register to output - i2c.write(i2c_address, &[0x00, 0x00])?; + fn init(&self, _i2c: &mut I2C, _i2c_address: u8) -> Result<(), I2C::Error> { Ok(()) } fn write_bits_to_gpio(&self, i2c: &mut I2C, i2c_address: u8) -> Result<(), I2C::Error> { - // first byte is GPIO register address - let data = [0x09, self.bits.0]; + let data = [self.bits()]; i2c.write(i2c_address, &data)?; Ok(()) } -} - -#[cfg(test)] -mod tests { - extern crate std; - use super::*; - use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction}; - - #[test] - fn test_generic_pcf8574t_config() { - let mut config = GenericPCF8574TConfig::::default(); - config.set_rs(1); - config.set_rw(0); - config.set_enable(1); - config.set_backlight(1); - config.set_data(0b1010); - - assert_eq!(config.bits(), 0b10101101); - assert_eq!( - GenericPCF8574TConfig::::default_i2c_address(), - 0x27 - ); - - config.set_rs(0); - config.set_rw(1); - config.set_enable(0); - config.set_backlight(0); - config.set_data(0b0101); - - assert_eq!(config.bits(), 0b01010010); - } - - #[test] - fn test_adafruit_lcd_backpack_config() { - let mut config = AdafruitLCDBackpackConfig::::default(); - config.set_rs(1); - config.set_enable(1); - config.set_backlight(1); - config.set_data(0b1010); - // adafruit backpack doesn't use RW - - assert_eq!(config.bits(), 0b11010110); - assert_eq!( - AdafruitLCDBackpackConfig::::default_i2c_address(), - 0x20 - ); - - config.set_rs(0); - config.set_enable(0); - config.set_backlight(0); - config.set_data(0b0101); - - assert_eq!(config.bits(), 0b00101000); - } - - #[test] - fn test_generic_pcf8574t_config_write_bits_to_gpio() { - let mut config = GenericPCF8574TConfig::::default(); - config.set_rs(1); - config.set_rw(0); - config.set_enable(1); - config.set_backlight(1); - config.set_data(0b1010); - - let expected_transactions = [I2cTransaction::write(0x27, std::vec![0b10101101])]; - let mut i2c = I2cMock::new(&expected_transactions); - config.write_bits_to_gpio(&mut i2c, 0x27).unwrap(); - i2c.done(); + fn device_count(&self) -> usize { + 1 } - #[test] - fn test_adafruit_lcd_backpack_config_write_bits_to_gpio() { - let mut config = AdafruitLCDBackpackConfig::::default(); - config.set_rs(1); - config.set_enable(1); - config.set_backlight(1); - config.set_data(0b1010); - - let expected_transactions = [I2cTransaction::write(0x20, std::vec![0x09, 0b11010110])]; - let mut i2c = I2cMock::new(&expected_transactions); - - config.write_bits_to_gpio(&mut i2c, 0x20).unwrap(); - i2c.done(); + /// Convert a row number to the row number on the device + fn row_to_device_row(&self, row: u8) -> (usize, u8) { + (0, row) } - #[test] - fn test_adafruit_init() { - let config = AdafruitLCDBackpackConfig::::default(); - - let expected_transactions = [I2cTransaction::write(0x20, std::vec![0x00, 0x00])]; - let mut i2c = I2cMock::new(&expected_transactions); - - config.init(&mut i2c, 0x20).unwrap(); - i2c.done(); - } + /// Determines of display type is supported by this adapter + fn is_supported(display_type: LcdDisplayType) -> bool; } diff --git a/src/adapter_config/adafruit_lcd_backpack.rs b/src/adapter_config/adafruit_lcd_backpack.rs new file mode 100644 index 0000000..8997c82 --- /dev/null +++ b/src/adapter_config/adafruit_lcd_backpack.rs @@ -0,0 +1,141 @@ +use bitfield::bitfield; +use core::marker::PhantomData; +use embedded_hal::i2c; + +use super::{AdapterConfigTrait, AdapterError, LcdDisplayType}; + +// Configuration for the MCP23008 based LCD backpack from Adafruit +bitfield! { + pub struct AdafruitLCDBackpackBitField(u8); + impl Debug; + impl BitAnd; + pub rs, set_rs: 1, 1; + pub enable, set_enable: 2, 2; + pub backlight, set_backlight: 7, 7; + pub data, set_data: 6, 3; +} + +pub struct AdafruitLCDBackpackConfig { + bits: AdafruitLCDBackpackBitField, + _marker: PhantomData, +} + +impl Default for AdafruitLCDBackpackConfig +where + I2C: i2c::I2c, +{ + fn default() -> Self { + Self { + bits: AdafruitLCDBackpackBitField(0), + _marker: PhantomData, + } + } +} +/// Configuration for the MCP23008 based LCD backpack from Adafruit +impl AdapterConfigTrait for AdafruitLCDBackpackConfig +where + I2C: i2c::I2c, +{ + fn bits(&self) -> u8 { + self.bits.0 + } + + fn default_i2c_address() -> u8 { + 0x20 + } + + fn set_rs(&mut self, value: bool) { + self.bits.set_rs(value as u8); + } + + /// Adafruit LCD Backpack doesn't use RW + fn set_rw(&mut self, _value: bool) { + // Not used + } + + fn set_enable(&mut self, value: bool, _device: usize) -> Result<(), AdapterError> { + self.bits.set_enable(value as u8); + Ok(()) + } + + fn set_backlight(&mut self, value: bool) { + self.bits.set_backlight(value as u8); + } + + fn set_data(&mut self, value: u8) { + self.bits.set_data(value); + } + + fn init(&self, i2c: &mut I2C, i2c_address: u8) -> Result<(), I2C::Error> { + // Set the MCP23008 IODIR register to output + i2c.write(i2c_address, &[0x00, 0x00])?; + Ok(()) + } + + fn write_bits_to_gpio(&self, i2c: &mut I2C, i2c_address: u8) -> Result<(), I2C::Error> { + // first byte is GPIO register address + let data = [0x09, self.bits.0]; + i2c.write(i2c_address, &data)?; + Ok(()) + } + + fn is_supported(display_type: LcdDisplayType) -> bool { + display_type != LcdDisplayType::Lcd40x4 + } +} + +#[cfg(test)] +mod tests { + extern crate std; + use super::*; + use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction}; + + #[test] + fn test_adafruit_lcd_backpack_config() { + let mut config = AdafruitLCDBackpackConfig::::default(); + config.set_rs(true); + assert!(config.set_enable(true, 2).is_ok()); + config.set_backlight(true); + config.set_data(0b1010); + // adafruit backpack doesn't use RW + + assert_eq!(config.bits(), 0b11010110); + assert_eq!( + AdafruitLCDBackpackConfig::::default_i2c_address(), + 0x20 + ); + + config.set_rs(false); + assert!(config.set_enable(false, 0).is_ok()); + config.set_backlight(false); + config.set_data(0b0101); + + assert_eq!(config.bits(), 0b00101000); + } + + #[test] + fn test_adafruit_lcd_backpack_config_write_bits_to_gpio() { + let mut config = AdafruitLCDBackpackConfig::::default(); + config.set_rs(true); + assert!(config.set_enable(true, 1).is_ok()); + config.set_backlight(true); + config.set_data(0b1010); + + let expected_transactions = [I2cTransaction::write(0x20, std::vec![0x09, 0b11010110])]; + let mut i2c = I2cMock::new(&expected_transactions); + + config.write_bits_to_gpio(&mut i2c, 0x20).unwrap(); + i2c.done(); + } + + #[test] + fn test_adafruit_init() { + let config = AdafruitLCDBackpackConfig::::default(); + + let expected_transactions = [I2cTransaction::write(0x20, std::vec![0x00, 0x00])]; + let mut i2c = I2cMock::new(&expected_transactions); + + config.init(&mut i2c, 0x20).unwrap(); + i2c.done(); + } +} diff --git a/src/adapter_config/dual_hd44780.rs b/src/adapter_config/dual_hd44780.rs new file mode 100644 index 0000000..193d39f --- /dev/null +++ b/src/adapter_config/dual_hd44780.rs @@ -0,0 +1,146 @@ +use bitfield::bitfield; +use core::marker::PhantomData; +use embedded_hal::i2c; + +use super::{AdapterConfigTrait, AdapterError, LcdDisplayType}; + +bitfield! { + pub struct DualHD44780_PCF8574TBitField(u8); + impl Debug; + impl BitAnd; + pub rs, set_rs: 0, 0; + pub enable2, set_enable2: 1, 1; + pub enable1, set_enable1: 2, 2; + pub backlight, set_backlight: 3, 3; + pub data, set_data: 7, 4; +} + +pub struct DualHD44780_PCF8574TConfig { + bits: DualHD44780_PCF8574TBitField, + _marker: PhantomData, +} + +impl Default for DualHD44780_PCF8574TConfig +where + I2C: i2c::I2c, +{ + fn default() -> Self { + Self { + bits: DualHD44780_PCF8574TBitField(0), + _marker: PhantomData, + } + } +} + +impl AdapterConfigTrait for DualHD44780_PCF8574TConfig +where + I2C: i2c::I2c, +{ + fn bits(&self) -> u8 { + self.bits.0 + } + + fn default_i2c_address() -> u8 { + 0x27 + } + + fn set_rs(&mut self, value: bool) { + self.bits.set_rs(value as u8); + } + + /// Dual HD44780 displays have two enable pins and do not use the RW pin + fn set_rw(&mut self, _value: bool) { + // does nothing + } + + fn set_enable(&mut self, value: bool, device: usize) -> Result<(), AdapterError> { + if device == 0 { + self.bits.set_enable1(value as u8); + } else if device == 1 { + self.bits.set_enable2(value as u8); + } else { + return Err(AdapterError::BadDeviceId); + } + Ok(()) + } + + fn set_backlight(&mut self, value: bool) { + self.bits.set_backlight(value as u8); + } + + fn set_data(&mut self, value: u8) { + self.bits.set_data(value); + } + + fn device_count(&self) -> usize { + 2 + } + + fn row_to_device_row(&self, row: u8) -> (usize, u8) { + if row < 2 { + (0, row) + } else { + (1, row - 2) + } + } + + fn is_supported(_display_type: LcdDisplayType) -> bool { + true + } +} + +#[cfg(test)] +mod tests { + extern crate std; + use super::*; + use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction}; + + #[test] + fn test_bad_device_id() { + let mut config = DualHD44780_PCF8574TConfig::::default(); + assert_eq!(config.set_enable(true, 2), Err(AdapterError::BadDeviceId)); + } + + #[test] + fn test_dual_hd44780_config() { + let mut config = DualHD44780_PCF8574TConfig::::default(); + config.set_rs(true); + config.set_rw(true); + assert!(config.set_enable(true, 0).is_ok()); + assert!(config.set_enable(false, 1).is_ok()); + config.set_backlight(true); + config.set_data(0b1010); + + assert_eq!(config.bits(), 0b10101101); + assert_eq!( + DualHD44780_PCF8574TConfig::::default_i2c_address(), + 0x27 + ); + + config.set_rs(false); + config.set_rw(true); + assert!(config.set_enable(false, 0).is_ok()); + assert!(config.set_enable(true, 1).is_ok()); + config.set_backlight(false); + config.set_data(0b0101); + + assert_eq!(config.bits(), 0b01010010); + } + + #[test] + fn test_dual_hd44780_config_write_bits_to_gpio() { + let mut config = DualHD44780_PCF8574TConfig::::default(); + config.set_rs(true); + config.set_rw(false); + assert!(config.set_enable(false, 0).is_ok()); + assert!(config.set_enable(true, 1).is_ok()); + config.set_backlight(false); + config.set_data(0b1010); + + let expected_transactions = [I2cTransaction::write(0x27, std::vec![0b10100011])]; + let mut i2c = I2cMock::new(&expected_transactions); + + config.write_bits_to_gpio(&mut i2c, 0x27).unwrap(); + i2c.done(); + } +} diff --git a/src/adapter_config/generic_pcf8574t.rs b/src/adapter_config/generic_pcf8574t.rs new file mode 100644 index 0000000..48a1a2f --- /dev/null +++ b/src/adapter_config/generic_pcf8574t.rs @@ -0,0 +1,138 @@ +use bitfield::bitfield; +use core::marker::PhantomData; +use embedded_hal::i2c; + +use crate::LcdDisplayType; + +use super::{AdapterConfigTrait, AdapterError}; + +// Configuration for the PCF8574T based 4-bit LCD interface sold +bitfield! { + pub struct GenericPCF8574TBitField(u8); + impl Debug; + impl BitAnd; + pub rs, set_rs: 0, 0; + pub rw, set_rw: 1, 1; + pub enable, set_enable: 2, 2; + pub backlight, set_backlight: 3, 3; + pub data, set_data: 7, 4; +} + +pub struct GenericPCF8574TConfig { + bits: GenericPCF8574TBitField, + _marker: PhantomData, +} + +impl Default for GenericPCF8574TConfig +where + I2C: i2c::I2c, +{ + fn default() -> Self { + Self { + bits: GenericPCF8574TBitField(0), + _marker: PhantomData, + } + } +} + +impl AdapterConfigTrait for GenericPCF8574TConfig +where + I2C: i2c::I2c, +{ + fn bits(&self) -> u8 { + self.bits.0 + } + + fn default_i2c_address() -> u8 { + 0x27 + } + + fn set_rs(&mut self, value: bool) { + self.bits.set_rs(value as u8); + } + + fn set_rw(&mut self, value: bool) { + self.bits.set_rw(value as u8); + } + + fn set_enable(&mut self, value: bool, _device: usize) -> Result<(), AdapterError> { + self.bits.set_enable(value as u8); + Ok(()) + } + + fn set_backlight(&mut self, value: bool) { + self.bits.set_backlight(value as u8); + } + + fn set_data(&mut self, value: u8) { + self.bits.set_data(value); + } + + fn is_supported(display_type: LcdDisplayType) -> bool { + display_type != LcdDisplayType::Lcd40x4 + } +} + +#[cfg(test)] +mod tests { + extern crate std; + use super::*; + use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction}; + + #[test] + fn test_generic_pcf8574t_compatiple_lcd_types() { + // not exhaustive for compatible displays (is_supported == true) + assert!(GenericPCF8574TConfig::::is_supported( + LcdDisplayType::Lcd16x2 + )); + assert!(GenericPCF8574TConfig::::is_supported( + LcdDisplayType::Lcd20x4 + )); + assert!(GenericPCF8574TConfig::::is_supported( + LcdDisplayType::Lcd40x2 + )); + assert!(!GenericPCF8574TConfig::::is_supported( + LcdDisplayType::Lcd40x4 + )); + } + + #[test] + fn test_generic_pcf8574t_config() { + let mut config = GenericPCF8574TConfig::::default(); + config.set_rs(true); + config.set_rw(false); + assert!(config.set_enable(true, 0).is_ok()); + config.set_backlight(true); + config.set_data(0b1010); + + assert_eq!(config.bits(), 0b10101101); + assert_eq!( + GenericPCF8574TConfig::::default_i2c_address(), + 0x27 + ); + + config.set_rs(false); + config.set_rw(true); + assert!(config.set_enable(false, 1).is_ok()); + config.set_backlight(false); + config.set_data(0b0101); + + assert_eq!(config.bits(), 0b01010010); + } + + #[test] + fn test_generic_pcf8574t_config_write_bits_to_gpio() { + let mut config = GenericPCF8574TConfig::::default(); + config.set_rs(true); + config.set_rw(false); + assert!(config.set_enable(true, 0).is_ok()); + config.set_backlight(false); + config.set_data(0b1010); + + let expected_transactions = [I2cTransaction::write(0x27, std::vec![0b10100101])]; + let mut i2c = I2cMock::new(&expected_transactions); + + config.write_bits_to_gpio(&mut i2c, 0x27).unwrap(); + i2c.done(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 938eaf9..f164647 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,10 @@ //! are supported: //! //! - **[Adafruit I2C/SPI LCD Backpack](https://www.adafruit.com/product/292)** - This is a simple I2C backpack that can be used with either I2C -//! or SPI. It is available from Adafruit and other retailers. This library only supports the I2C interface. +//! or SPI. It is available from Adafruit and other retailers. This library only supports the I2C interface. //! - **PCF8574-based I2C adapter** - These adapters are ubiquitous on eBay and AliExpress and have no clear branding. The most common pin -//! wiring uses 4 data pins and 3 control pins. Most models have the display 4-bit data pins connected to P4-P7 of the PCF8574. This library -//! supports that configuration, though it would be straightforward to add support for other configurations. +//! wiring uses 4 data pins and 3 control pins. Most models have the display 4-bit data pins connected to P4-P7 of the PCF8574. This library +//! supports that configuration, though it would be straightforward to add support for other configurations. //! //! Key features include: //! - Convenient high-level API for controlling the display @@ -14,6 +14,7 @@ //! - Backlight control //! - `core::fmt::Write` implementation for easy use with the `write!` macro //! - Compatible with the `embedded-hal` traits v1.0 and later +//! - Support for character displays that used multiple HD44780 drivers, such as the 40x4 display //! //! ## Usage //! Add this to your `Cargo.toml`: @@ -39,6 +40,8 @@ //! let mut lcd = AdafruitLCDBackpack::new(i2c, LcdDisplayType::Lcd16x2, delay); //! // PCF8574T adapter //! let mut lcd = CharacterDisplayPCF8574T::new(i2c, LcdDisplayType::Lcd16x2, delay); +//! // Character display with dual HD44780 drivers using a PCF8574T I2C adapter +//! let mut lcd = CharacterDisplayDualHD44780::new(i2c, LcdDisplayType::Lcd40x4, delay); //! ``` //! When creating the display object, you can choose the display type from the `LcdDisplayType` enum. The display type should match the physical //! display you are using. This display type configures the number of rows and columns, and the internal row offsets for the display. @@ -110,6 +113,9 @@ const LCD_FLAG_5x8_DOTS: u8 = 0x00; // 8 pixel high font mode mod adapter_config; +const MAX_DEVICE_COUNT: usize = 2; + +#[derive(Debug, PartialEq, Copy, Clone)] /// Errors that can occur when using the LCD backpack pub enum Error where @@ -123,6 +129,10 @@ where ColumnOutOfRange, /// Formatting error FormattingError(core::fmt::Error), + /// Dive Adapter Error + AdapterError(adapter_config::AdapterError), + /// The discplay type is not compatible with specific adapter. + UnsupportedDisplayType, } impl From for Error @@ -134,6 +144,15 @@ where } } +impl From for Error +where + I2C: i2c::I2c, +{ + fn from(err: adapter_config::AdapterError) -> Self { + Error::AdapterError(err) + } +} + #[cfg(feature = "defmt")] impl defmt::Format for Error where @@ -145,10 +164,13 @@ where Error::RowOutOfRange => defmt::write!(fmt, "Row out of range"), Error::ColumnOutOfRange => defmt::write!(fmt, "Column out of range"), Error::FormattingError(_e) => defmt::write!(fmt, "Formatting error"), + Error::AdapterError(e) => defmt::write!(fmt, "Adapter error: {}", e), + Error::UnsupportedDisplayType => defmt::write!(fmt, "Unsupported display type"), } } } +#[derive(Debug, PartialEq, Clone, Copy)] /// The type of LCD display. This is used to determine the number of rows and columns, and the row offsets. pub enum LcdDisplayType { /// 20x4 display @@ -163,6 +185,8 @@ pub enum LcdDisplayType { Lcd8x2, /// 40x2 display Lcd40x2, + /// 40x4 display + Lcd40x4, } impl LcdDisplayType { @@ -175,6 +199,7 @@ impl LcdDisplayType { LcdDisplayType::Lcd16x4 => 4, LcdDisplayType::Lcd8x2 => 2, LcdDisplayType::Lcd40x2 => 2, + LcdDisplayType::Lcd40x4 => 4, } } @@ -187,6 +212,7 @@ impl LcdDisplayType { LcdDisplayType::Lcd16x4 => 16, LcdDisplayType::Lcd8x2 => 8, LcdDisplayType::Lcd40x2 => 40, + LcdDisplayType::Lcd40x4 => 40, } } @@ -200,34 +226,58 @@ impl LcdDisplayType { LcdDisplayType::Lcd16x4 => [0x00, 0x40, 0x10, 0x50], LcdDisplayType::Lcd8x2 => [0x00, 0x40, 0x00, 0x40], LcdDisplayType::Lcd40x2 => [0x00, 0x40, 0x00, 0x40], + LcdDisplayType::Lcd40x4 => [0x00, 0x40, 0x00, 0x40], } } } -pub struct BaseCharacterDisplay { +pub struct BaseCharacterDisplay +where + I2C: i2c::I2c, + DELAY: DelayNs, + DEVICE: adapter_config::AdapterConfigTrait, +{ lcd_type: LcdDisplayType, i2c: I2C, address: u8, - bits: BITS, + device: DEVICE, delay: DELAY, - display_function: u8, - display_control: u8, - display_mode: u8, + display_function: [u8; MAX_DEVICE_COUNT], + display_control: [u8; MAX_DEVICE_COUNT], + display_mode: [u8; MAX_DEVICE_COUNT], + active_device: usize, } -pub type CharacterDisplayPCF8574T = - BaseCharacterDisplay>; -pub type AdafruitLCDBackpack = - BaseCharacterDisplay>; - -impl BaseCharacterDisplay +/// Character display using a generic PCF8574T I2C adapter. +pub type CharacterDisplayPCF8574T = BaseCharacterDisplay< + I2C, + DELAY, + crate::adapter_config::generic_pcf8574t::GenericPCF8574TConfig, +>; + +/// Character display using an Adafruit I2C/SPI LCD backpack. +pub type AdafruitLCDBackpack = BaseCharacterDisplay< + I2C, + DELAY, + crate::adapter_config::adafruit_lcd_backpack::AdafruitLCDBackpackConfig, +>; + +/// Character display using dual HD44780 I2C drivers connected using a generic PCF8574T I2C adapter with a pinout that +/// has two enable pins, one for each HD44780 driver. Typically used for 40x4 character displays. +pub type CharacterDisplayDualHD44780 = BaseCharacterDisplay< + I2C, + DELAY, + crate::adapter_config::dual_hd44780::DualHD44780_PCF8574TConfig, +>; + +impl BaseCharacterDisplay where I2C: i2c::I2c, DELAY: DelayNs, - BITS: adapter_config::AdapterConfigTrait, + DEVICE: adapter_config::AdapterConfigTrait, { pub fn new(i2c: I2C, lcd_type: LcdDisplayType, delay: DELAY) -> Self { - Self::new_with_address(i2c, BITS::default_i2c_address(), lcd_type, delay) + Self::new_with_address(i2c, DEVICE::default_i2c_address(), lcd_type, delay) } pub fn new_with_address(i2c: I2C, address: u8, lcd_type: LcdDisplayType, delay: DELAY) -> Self { @@ -235,34 +285,56 @@ where lcd_type, i2c, address, - bits: BITS::default(), + device: DEVICE::default(), delay, - display_function: LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE, - display_control: LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF, - display_mode: LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT, + display_function: [0; MAX_DEVICE_COUNT], + display_control: [LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF; + MAX_DEVICE_COUNT], + display_mode: [LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT; MAX_DEVICE_COUNT], + active_device: 0, } } pub fn init(&mut self) -> Result<(), Error> { - self.bits + if !DEVICE::is_supported(self.lcd_type) { + return Err(Error::UnsupportedDisplayType); + } + + self.device .init(&mut self.i2c, self.address) .map_err(Error::I2cError)?; - // Put LCD into 4 bit mode, device starts in 8 bit mode - self.write_4_bits(0x03)?; - self.delay.delay_ms(5); - self.write_4_bits(0x03)?; - self.delay.delay_ms(5); - self.write_4_bits(0x03)?; - self.delay.delay_us(150); - self.write_4_bits(0x02)?; - + for device in 0..self.device.device_count() { + if device >= MAX_DEVICE_COUNT { + return Err(Error::AdapterError( + adapter_config::AdapterError::BadDeviceId, + )); + } + self.display_function[device] = LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE; + + // Put LCD into 4 bit mode, device starts in 8 bit mode + self.write_4_bits(0x03, device)?; + self.delay.delay_ms(5); + self.write_4_bits(0x03, device)?; + self.delay.delay_ms(5); + self.write_4_bits(0x03, device)?; + self.delay.delay_us(150); + self.write_4_bits(0x02, device)?; + + self.send_command_to_device( + LCD_CMD_FUNCTIONSET | self.display_function[device], + device, + )?; + self.send_command_to_device( + LCD_CMD_DISPLAYCONTROL | self.display_control[device], + device, + )?; + self.send_command_to_device(LCD_CMD_ENTRYMODESET | self.display_mode[device], device)?; + self.clear_device(device)?.home_device(device)?; + } // set up the display - self.bits.set_backlight(1); - self.send_command(LCD_CMD_FUNCTIONSET | self.display_function)?; - self.send_command(LCD_CMD_DISPLAYCONTROL | self.display_control)?; - self.send_command(LCD_CMD_ENTRYMODESET | self.display_mode)?; - self.clear()?.home()?; + self.backlight(true)?; + self.active_device = 0; Ok(()) } @@ -272,33 +344,39 @@ where } fn send_command(&mut self, command: u8) -> Result<(), Error> { - self.bits.set_rs(0); - self.write_8_bits(command)?; + self.send_command_to_device(command, 0) + } + fn send_command_to_device(&mut self, command: u8, device: usize) -> Result<(), Error> { + self.device.set_rs(false); + self.write_8_bits(command, device)?; Ok(()) } fn write_data(&mut self, data: u8) -> Result<(), Error> { - self.bits.set_rs(1); - self.write_8_bits(data)?; + self.write_data_to_device(data, 0) + } + fn write_data_to_device(&mut self, data: u8, device: usize) -> Result<(), Error> { + self.device.set_rs(true); + self.write_8_bits(data, device)?; Ok(()) } - fn write_8_bits(&mut self, value: u8) -> Result<(), Error> { - self.write_4_bits(value >> 4)?; - self.write_4_bits(value & 0x0F)?; + fn write_8_bits(&mut self, value: u8, device: usize) -> Result<(), Error> { + self.write_4_bits(value >> 4, device)?; + self.write_4_bits(value & 0x0F, device)?; Ok(()) } - fn write_4_bits(&mut self, value: u8) -> Result<(), Error> { - self.bits.set_data(value & 0x0F); - self.bits.set_rw(0); - self.bits.set_enable(1); - self.bits + fn write_4_bits(&mut self, value: u8, device: usize) -> Result<(), Error> { + self.device.set_data(value & 0x0F); + self.device.set_rw(false); + self.device.set_enable(true, device)?; + self.device .write_bits_to_gpio(&mut self.i2c, self.address) .map_err(Error::I2cError)?; self.delay.delay_us(1); - self.bits.set_enable(0); - self.bits + self.device.set_enable(false, device)?; + self.device .write_bits_to_gpio(&mut self.i2c, self.address) .map_err(Error::I2cError)?; self.delay.delay_us(1); @@ -310,21 +388,48 @@ where //-------------------------------------------------------------------------------------------------- /// Clear the display + /// For multiple devices, this clears all devices pub fn clear(&mut self) -> Result<&mut Self, Error> { - self.send_command(LCD_CMD_CLEARDISPLAY)?; + for device in 0..self.device.device_count() { + self.clear_device(device)?; + } + Ok(self) + } + + pub fn clear_device(&mut self, device: usize) -> Result<&mut Self, Error> { + self.send_command_to_device(LCD_CMD_CLEARDISPLAY, device)?; self.delay.delay_ms(2); Ok(self) } /// Set the cursor to the home position + /// For multiple devices, this sets the cursor to the home position on the 0 device + /// and sets the active device to 0 pub fn home(&mut self) -> Result<&mut Self, Error> { - self.send_command(LCD_CMD_RETURNHOME)?; + self.active_device = 0; + self.home_device(0) + } + + pub fn home_device(&mut self, device: usize) -> Result<&mut Self, Error> { + self.send_command_to_device(LCD_CMD_RETURNHOME, device)?; self.delay.delay_ms(2); Ok(self) } - /// Set the cursor position at specified column and row + /// Set the cursor position at specified column and row, starting at 0. + /// For multiple devices, this sets the active device to the device containing the row. pub fn set_cursor(&mut self, col: u8, row: u8) -> Result<&mut Self, Error> { + let (device, device_row) = self.device.row_to_device_row(row); + self.active_device = device; + self.set_cursor_device(col, device_row, self.active_device) + } + + pub fn set_cursor_device( + &mut self, + col: u8, + row: u8, + device: usize, + ) -> Result<&mut Self, Error> { if row >= self.lcd_type.rows() { return Err(Error::RowOutOfRange); } @@ -332,103 +437,210 @@ where return Err(Error::ColumnOutOfRange); } - self.send_command( + self.send_command_to_device( LCD_CMD_SETDDRAMADDR | (col + self.lcd_type.row_offsets()[row as usize]), + device, )?; Ok(self) } - /// Set the cursor visibility + /// Set the cursor visibility. + /// For multiple devices, this sets the cursor visibility on the active device. pub fn show_cursor(&mut self, show_cursor: bool) -> Result<&mut Self, Error> { + self.show_cursor_device(show_cursor, self.active_device) + } + + pub fn show_cursor_device( + &mut self, + show_cursor: bool, + device: usize, + ) -> Result<&mut Self, Error> { if show_cursor { - self.display_control |= LCD_FLAG_CURSORON; + self.display_control[device] |= LCD_FLAG_CURSORON; } else { - self.display_control &= !LCD_FLAG_CURSORON; + self.display_control[device] &= !LCD_FLAG_CURSORON; } - self.send_command(LCD_CMD_DISPLAYCONTROL | self.display_control)?; + self.send_command_to_device( + LCD_CMD_DISPLAYCONTROL | self.display_control[device], + device, + )?; Ok(self) } - /// Set the cursor blinking + /// Set the cursor blinking. + /// For multiple devices, this sets the cursor blinking on the active device. pub fn blink_cursor(&mut self, blink_cursor: bool) -> Result<&mut Self, Error> { + self.blink_cursor_device(blink_cursor, self.active_device) + } + + pub fn blink_cursor_device( + &mut self, + blink_cursor: bool, + device: usize, + ) -> Result<&mut Self, Error> { if blink_cursor { - self.display_control |= LCD_FLAG_BLINKON; + self.display_control[device] |= LCD_FLAG_BLINKON; } else { - self.display_control &= !LCD_FLAG_BLINKON; + self.display_control[device] &= !LCD_FLAG_BLINKON; } - self.send_command(LCD_CMD_DISPLAYCONTROL | self.display_control)?; + self.send_command_to_device( + LCD_CMD_DISPLAYCONTROL | self.display_control[device], + device, + )?; Ok(self) } - /// Set the display visibility + /// Set the display visibility. + /// For multiple devices, this sets the display visibility on all devices. pub fn show_display(&mut self, show_display: bool) -> Result<&mut Self, Error> { + for device in 0..self.device.device_count() { + self.show_display_device(show_display, device)?; + } + Ok(self) + } + + pub fn show_display_device( + &mut self, + show_display: bool, + device: usize, + ) -> Result<&mut Self, Error> { if show_display { - self.display_control |= LCD_FLAG_DISPLAYON; + self.display_control[device] |= LCD_FLAG_DISPLAYON; } else { - self.display_control &= !LCD_FLAG_DISPLAYON; + self.display_control[device] &= !LCD_FLAG_DISPLAYON; } - self.send_command(LCD_CMD_DISPLAYCONTROL | self.display_control)?; + self.send_command_to_device( + LCD_CMD_DISPLAYCONTROL | self.display_control[device], + device, + )?; Ok(self) } - /// Scroll the display to the left + /// Scroll the display to the left. + /// For multiple devices, this scrolls all devices to the left. pub fn scroll_display_left(&mut self) -> Result<&mut Self, Error> { - self.send_command(LCD_CMD_CURSORSHIFT | LCD_FLAG_DISPLAYMOVE | LCD_FLAG_MOVELEFT)?; + for device in 0..self.device.device_count() { + self.scroll_display_left_device(device)?; + } + Ok(self) + } + + pub fn scroll_display_left_device(&mut self, device: usize) -> Result<&mut Self, Error> { + self.send_command_to_device( + LCD_CMD_CURSORSHIFT | LCD_FLAG_DISPLAYMOVE | LCD_FLAG_MOVELEFT, + device, + )?; Ok(self) } - /// Scroll the display to the right + /// Scroll the display to the right. + /// For multiple devices, this scrolls all devices to the right. pub fn scroll_display_right(&mut self) -> Result<&mut Self, Error> { - self.send_command(LCD_CMD_CURSORSHIFT | LCD_FLAG_DISPLAYMOVE | LCD_FLAG_MOVERIGHT)?; + for device in 0..self.device.device_count() { + self.scroll_display_right_device(device)?; + } + Ok(self) + } + + pub fn scroll_display_right_device(&mut self, device: usize) -> Result<&mut Self, Error> { + self.send_command_to_device( + LCD_CMD_CURSORSHIFT | LCD_FLAG_DISPLAYMOVE | LCD_FLAG_MOVERIGHT, + device, + )?; Ok(self) } - /// Set the text flow direction to left to right + /// Set the text flow direction to left to right. + /// For multiple devices, this sets the text flow direction to left to right on all devices. pub fn left_to_right(&mut self) -> Result<&mut Self, Error> { - self.display_mode |= LCD_FLAG_ENTRYLEFT; - self.send_command(LCD_CMD_ENTRYMODESET | self.display_mode)?; + for device in 0..self.device.device_count() { + self.left_to_right_device(device)?; + } + Ok(self) + } + + pub fn left_to_right_device(&mut self, device: usize) -> Result<&mut Self, Error> { + self.display_mode[device] |= LCD_FLAG_ENTRYLEFT; + self.send_command_to_device(LCD_CMD_ENTRYMODESET | self.display_mode[device], device)?; Ok(self) } - /// Set the text flow direction to right to left + /// Set the text flow direction to right to left. + /// For multiple devices, this sets the text flow direction to right to left on all devices. pub fn right_to_left(&mut self) -> Result<&mut Self, Error> { - self.display_mode &= !LCD_FLAG_ENTRYLEFT; - self.send_command(LCD_CMD_ENTRYMODESET | self.display_mode)?; + for device in 0..self.device.device_count() { + self.right_to_left_device(device)?; + } + Ok(self) + } + + pub fn right_to_left_device(&mut self, device: usize) -> Result<&mut Self, Error> { + self.display_mode[device] &= !LCD_FLAG_ENTRYLEFT; + self.send_command_to_device(LCD_CMD_ENTRYMODESET | self.display_mode[device], device)?; Ok(self) } - /// Set the auto scroll mode + /// Set the auto scroll mode. + /// For multiple devices, this sets the auto scroll mode on all devices. pub fn autoscroll(&mut self, autoscroll: bool) -> Result<&mut Self, Error> { + for device in 0..self.device.device_count() { + self.autoscroll_device(autoscroll, device)?; + } + Ok(self) + } + + pub fn autoscroll_device( + &mut self, + autoscroll: bool, + device: usize, + ) -> Result<&mut Self, Error> { if autoscroll { - self.display_mode |= LCD_FLAG_ENTRYSHIFTINCREMENT; + self.display_mode[device] |= LCD_FLAG_ENTRYSHIFTINCREMENT; } else { - self.display_mode &= !LCD_FLAG_ENTRYSHIFTINCREMENT; + self.display_mode[device] &= !LCD_FLAG_ENTRYSHIFTINCREMENT; } - self.send_command(LCD_CMD_ENTRYMODESET | self.display_mode)?; + self.send_command_to_device(LCD_CMD_ENTRYMODESET | self.display_mode[device], device)?; Ok(self) } - /// Create a new custom character + /// Create a new custom character. + /// For multiple devices, this creates the custom character on all devices. pub fn create_char(&mut self, location: u8, charmap: [u8; 8]) -> Result<&mut Self, Error> { - self.send_command(LCD_CMD_SETCGRAMADDR | ((location & 0x7) << 3))?; + for device in 0..self.device.device_count() { + self.create_char_device(location, charmap, device)?; + } + Ok(self) + } + + pub fn create_char_device( + &mut self, + location: u8, + charmap: [u8; 8], + device: usize, + ) -> Result<&mut Self, Error> { + self.send_command_to_device(LCD_CMD_SETCGRAMADDR | ((location & 0x7) << 3), device)?; for &charmap_byte in charmap.iter() { - self.write_data(charmap_byte)?; + self.write_data_to_device(charmap_byte, device)?; } Ok(self) } - /// Prints a string to the LCD at the current cursor position + /// Prints a string to the LCD at the current cursor position of the active device. pub fn print(&mut self, text: &str) -> Result<&mut Self, Error> { + self.print_device(text, self.active_device) + } + + pub fn print_device(&mut self, text: &str, device: usize) -> Result<&mut Self, Error> { for c in text.chars() { - self.write_data(c as u8)?; + self.write_data_to_device(c as u8, device)?; } Ok(self) } /// Turn the backlight on or off pub fn backlight(&mut self, on: bool) -> Result<&mut Self, Error> { - self.bits.set_backlight(on as u8); - self.bits + self.device.set_backlight(on); + self.device .write_bits_to_gpio(&mut self.i2c, self.address) .map_err(Error::I2cError)?; Ok(self) @@ -436,6 +648,8 @@ where } /// Implement the `core::fmt::Write` trait for the LCD backpack, allowing it to be used with the `write!` macro. +/// This is a convenience method for printing to the display. For multi-device, this will print to the active device as set by +/// `set_cursor`. If you need to print to a specific device, use the `print` method. impl core::fmt::Write for BaseCharacterDisplay where I2C: i2c::I2c, @@ -451,7 +665,7 @@ where } #[cfg(test)] -mod tests { +mod lib_tests { extern crate std; use super::*; use embedded_hal_mock::eh1::{ @@ -479,34 +693,36 @@ mod tests { // I2cTransaction::write(i2c_address, std::vec![0b0000_1000]), // backlight on // LCD_CMD_FUNCTIONSET | LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE // = 0x20 | 0x00 | 0x00 | 0x08 = 0x28 - I2cTransaction::write(i2c_address, std::vec![0b0010_1100]), // high nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0b0010_1000]), // high nibble, rw=0, enable=0 - I2cTransaction::write(i2c_address, std::vec![0b1000_1100]), // low nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0b1000_1000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b1000_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b1000_0000]), // low nibble, rw=0, enable=0 // LCD_CMD_DISPLAYCONTROL | LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF // = 0x08 | 0x04 | 0x00 | 0x00 = 0x0C - I2cTransaction::write(i2c_address, std::vec![0b0000_1100]), // high nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0b0000_1000]), // high nibble, rw=0, enable=0 - I2cTransaction::write(i2c_address, std::vec![0b1100_1100]), // low nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0b1100_1000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b1100_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b1100_0000]), // low nibble, rw=0, enable=0 // LCD_CMD_ENTRYMODESET | LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT // = 0x04 | 0x02 | 0x00 = 0x06 - I2cTransaction::write(i2c_address, std::vec![0b0000_1100]), // high nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0b0000_1000]), // high nibble, rw=0, enable=0 - I2cTransaction::write(i2c_address, std::vec![0b0110_1100]), // low nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0b0110_1000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0110_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0110_0000]), // low nibble, rw=0, enable=0 // LCD_CMD_CLEARDISPLAY // = 0x01 - I2cTransaction::write(i2c_address, std::vec![0b0000_1100]), // high nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0b0000_1000]), // high nibble, rw=0, enable=0 - I2cTransaction::write(i2c_address, std::vec![0b0001_1100]), // low nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0b0001_1000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0001_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0001_0000]), // low nibble, rw=0, enable=0 // LCD_CMD_RETURNHOME // = 0x02 - I2cTransaction::write(i2c_address, std::vec![0b0000_1100]), // high nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0b0000_1000]), // high nibble, rw=0, enable=0 - I2cTransaction::write(i2c_address, std::vec![0b0010_1100]), // low nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0b0010_1000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // low nibble, rw=0, enable=0 + // Set Backlight + I2cTransaction::write(i2c_address, std::vec![0b0010_1000]), // backlight on ]; let i2c = I2cMock::new(&expected_i2c_transactions); @@ -540,34 +756,36 @@ mod tests { // I2cTransaction::write(i2c_address, std::vec![0b0000_1000]), // backlight on // LCD_CMD_FUNCTIONSET | LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE // = 0x20 | 0x00 | 0x00 | 0x08 = 0x28 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0010_100]), // high nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0010_000]), // high nibble, rw=0, enable=0 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_1000_100]), // low nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_1000_000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0010_100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0010_000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_1000_100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_1000_000]), // low nibble, rw=0, enable=0 // LCD_CMD_DISPLAYCONTROL | LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF // = 0x08 | 0x04 | 0x00 | 0x00 = 0x0C - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0000_100]), // high nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0000_000]), // high nibble, rw=0, enable=0 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_1100_100]), // low nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_1100_000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_1100_100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_1100_000]), // low nibble, rw=0, enable=0 // LCD_CMD_ENTRYMODESET | LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT // = 0x04 | 0x02 | 0x00 = 0x06 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0000_100]), // high nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0000_000]), // high nibble, rw=0, enable=0 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0110_100]), // low nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0110_000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0110_100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0110_000]), // low nibble, rw=0, enable=0 // LCD_CMD_CLEARDISPLAY // = 0x01 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0000_100]), // high nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0000_000]), // high nibble, rw=0, enable=0 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0001_100]), // low nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0001_000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0001_100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0001_000]), // low nibble, rw=0, enable=0 // LCD_CMD_RETURNHOME // = 0x02 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0000_100]), // high nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0000_000]), // high nibble, rw=0, enable=0 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0010_100]), // low nibble, rw=0, enable=1 - I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0010_000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0010_100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0010_000]), // low nibble, rw=0, enable=0 + // Set Backlight + I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0010_000]), // backlight on ]; let i2c = I2cMock::new(&expected_i2c_transactions); @@ -578,4 +796,111 @@ mod tests { // finish the i2c mock lcd.i2c().done(); } + + #[test] + fn test_character_display_dual_hd44780_init() { + let i2c_address = 0x27_u8; + let expected_i2c_transactions = std::vec![ + // the PCF8574T has no adapter init sequence, so nothing to prepend + // *** Device 0 *** + // the LCD init sequence for device 0 + // write low nibble of 0x03 3 times + I2cTransaction::write(i2c_address, std::vec![0b0011_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0011_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0011_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0 + // write high nibble of 0x02 one time + I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0 + // turn on the backlight + // I2cTransaction::write(i2c_address, std::vec![0b0000_1000]), // backlight on + // LCD_CMD_FUNCTIONSET | LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE + // = 0x20 | 0x00 | 0x00 | 0x08 = 0x28 + I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b1000_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b1000_0000]), // low nibble, rw=0, enable=0 + // LCD_CMD_DISPLAYCONTROL | LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF + // = 0x08 | 0x04 | 0x00 | 0x00 = 0x0C + I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b1100_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b1100_0000]), // low nibble, rw=0, enable=0 + // LCD_CMD_ENTRYMODESET | LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT + // = 0x04 | 0x02 | 0x00 = 0x06 + I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0110_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0110_0000]), // low nibble, rw=0, enable=0 + // LCD_CMD_CLEARDISPLAY + // = 0x01 + I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0001_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0001_0000]), // low nibble, rw=0, enable=0 + // LCD_CMD_RETURNHOME + // = 0x02 + I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // low nibble, rw=0, enable=0 + // *** Device 1 *** + // the LCD init sequence for device 0 + // write low nibble of 0x03 3 times + I2cTransaction::write(i2c_address, std::vec![0b0011_0010]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0011_0010]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0011_0010]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0 + // write high nibble of 0x02 one time + I2cTransaction::write(i2c_address, std::vec![0b0010_0010]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0 + // turn on the backlight + // I2cTransaction::write(i2c_address, std::vec![0b0000_1000]), // backlight on + // LCD_CMD_FUNCTIONSET | LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE + // = 0x20 | 0x00 | 0x00 | 0x08 = 0x28 + I2cTransaction::write(i2c_address, std::vec![0b0010_0010]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b1000_0010]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b1000_0000]), // low nibble, rw=0, enable=0 + // LCD_CMD_DISPLAYCONTROL | LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF + // = 0x08 | 0x04 | 0x00 | 0x00 = 0x0C + I2cTransaction::write(i2c_address, std::vec![0b0000_0010]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b1100_0010]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b1100_0000]), // low nibble, rw=0, enable=0 + // LCD_CMD_ENTRYMODESET | LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT + // = 0x04 | 0x02 | 0x00 = 0x06 + I2cTransaction::write(i2c_address, std::vec![0b0000_0010]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0110_0010]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0110_0000]), // low nibble, rw=0, enable=0 + // LCD_CMD_CLEARDISPLAY + // = 0x01 + I2cTransaction::write(i2c_address, std::vec![0b0000_0010]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0001_0010]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0001_0000]), // low nibble, rw=0, enable=0 + // LCD_CMD_RETURNHOME + // = 0x02 + I2cTransaction::write(i2c_address, std::vec![0b0000_0010]), // high nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0 + I2cTransaction::write(i2c_address, std::vec![0b0010_0010]), // low nibble, rw=0, enable=1 + I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // low nibble, rw=0, enable=0 + // Set Backlight + I2cTransaction::write(i2c_address, std::vec![0b0010_1000]), // backlight on + ]; + + let i2c = I2cMock::new(&expected_i2c_transactions); + let mut lcd = + CharacterDisplayDualHD44780::new(i2c, LcdDisplayType::Lcd40x4, NoopDelay::new()); + let result = lcd.init(); + assert!(result.is_ok()); + + // finish the i2c mock + lcd.i2c().done(); + } }