diff --git a/CHANGELOG.md b/CHANGELOG.md index b99eea3..7239200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## [0.3.0] - 2024-11-03 +* Added support for data reads from the LCD Character Display for I2C adapters that support it. Refactored code to support this. +* Improved docuemntation + ## [0.2.1] - 2024-10-26 * Added support for `ufmt` * Added `display_type()` method to `BaseCharacterDisplay` to allow for querying the display type diff --git a/Cargo.toml b/Cargo.toml index 706cb30..bb7baca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "i2c-character-display" -version = "0.2.1" +version = "0.3.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 f5c7ac4..07c763c 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Key features include: - `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 uses multiple HD44780 drivers, such as the 40x4 display +- Optional support for the `defmt` and `ufmt` logging frameworks +- Optional support for reading from the display on adapters that support it ## Usage Add this to your `Cargo.toml`: @@ -82,6 +84,11 @@ of commands. For example: ```rust lcd.backlight(true)?.clear()?.home()?.print("Hello, world!")?; ``` +### Reading from the display +Some I2C adapters support reading data from the HD44780 controller. Dor the I2C adapters that support it, the `read_device_data` method can be used to read +from either the CGRAM or DDRAM at the current cursor position. The `read_address_counter` method can be used to read the address counter from the HD44780 controller. +In both cases, the specific meaning of the data depends on the prior commands sent to the display. See the HD44780 datasheet for more information. + ### Multiple HD44780 controller character displays Some character displays, such as the 40x4 display, use two HD44780 controllers to drive the display. This library supports these displays by treating them as one logical display with multiple HD44780 controllers. The `CharacterDisplayDualHD44780` type is used to control these displays. diff --git a/src/adapter_config.rs b/src/adapter_config.rs index ee22b2e..87ec9da 100644 --- a/src/adapter_config.rs +++ b/src/adapter_config.rs @@ -2,32 +2,59 @@ pub mod adafruit_lcd_backpack; pub mod dual_hd44780; pub mod generic_pcf8574t; +use core::fmt::Display; + use crate::LcdDisplayType; use embedded_hal::i2c; #[derive(Debug, PartialEq, Copy, Clone)] -pub enum AdapterError { +pub enum AdapterError +where + I2C: i2c::I2c, +{ /// The device ID was not recognized BadDeviceId, + /// An I2C error occurred + I2CError(I2C::Error), } #[cfg(feature = "defmt")] -impl defmt::Format for AdapterError { +impl defmt::Format for AdapterError +where + I2C: i2c::I2c, +{ fn format(&self, fmt: defmt::Formatter) { match self { AdapterError::BadDeviceId => defmt::write!(fmt, "BadDeviceId"), + AdapterError::I2CError(_) => defmt::write!(fmt, "I2CError"), } } } #[cfg(feature = "ufmt")] -impl ufmt::uDisplay for AdapterError { +impl ufmt::uDisplay for AdapterError +where + I2C: i2c::I2c, +{ fn fmt(&self, w: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> where W: ufmt::uWrite + ?Sized, { match self { AdapterError::BadDeviceId => ufmt::uwrite!(w, "BadDeviceId"), + AdapterError::I2CError(_) => ufmt::uwrite!(w, "I2CError"), + } + } +} + +impl Display for AdapterError +where + I2C: i2c::I2c, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + AdapterError::BadDeviceId => write!(f, "BadDeviceId"), + AdapterError::I2CError(_) => write!(f, "I2CError"), } } } @@ -36,28 +63,111 @@ pub trait AdapterConfigTrait: Default where I2C: i2c::I2c, { + /// Returns the bitfield value for the adapter fn bits(&self) -> u8; + + /// Returns the default I2C address for the adapter fn default_i2c_address() -> u8; + /// Determines if reading from device is supported by this adapter + fn supports_reads() -> bool { + false + } + + /// Sets the RS pin for the display. A value of `false` indicates an instruction is being sent, while + /// a value of `true` indicates data is being sent. fn set_rs(&mut self, value: bool); + + /// Sets the RW pin for the display. A value of `false` indicates a write operation, while a value of + /// `true` indicates a read operation. Not all displays support reading, so this method may not be + /// implemented fully. 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_enable(&mut self, value: bool, device: usize) -> Result<(), AdapterError>; + + /// Sets the backlight pin for the display. A value of `true` indicates the backlight is on, while a value + /// of `false` indicates the backlight is off. 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> { Ok(()) } - fn write_bits_to_gpio(&self, i2c: &mut I2C, i2c_address: u8) -> Result<(), I2C::Error> { + fn write_bits_to_gpio(&self, i2c: &mut I2C, i2c_address: u8) -> Result<(), AdapterError> { let data = [self.bits()]; - i2c.write(i2c_address, &data)?; + i2c.write(i2c_address, &data) + .map_err(AdapterError::I2CError)?; + Ok(()) + } + + /// writes a full byte to the indicated device. If `rs_setting` is `true`, the data is written to the data register, + /// either the CGRAM or DDRAM, depending on prior command sent. If `rs_setting` is `false`, the data is written to + /// command register. + fn write_byte_to_device( + &mut self, + i2c: &mut I2C, + i2c_address: u8, + device: usize, + rs_setting: bool, + value: u8, + ) -> Result<(), AdapterError> { + self.write_nibble_to_device(i2c, i2c_address, device, rs_setting, value >> 4) + .and_then(|_| { + self.write_nibble_to_device(i2c, i2c_address, device, rs_setting, value & 0x0F) + }) + } + + /// writes the lower nibble of a `value` byte to the indicated device. Typically only used for device initialization in 4 bit mode. + /// If `rs_setting` is `true`, the data is written to the data register, + /// either the CGRAM or DDRAM, depending on prior command sent. If `rs_setting` is `false`, the data is written to + /// command register. + fn write_nibble_to_device( + &mut self, + i2c: &mut I2C, + i2c_address: u8, + device: usize, + rs_setting: bool, + value: u8, + ) -> Result<(), AdapterError> { + self.set_rs(rs_setting); + self.set_rw(false); + + // now write the low nibble + self.set_data(value & 0x0F); + self.set_enable(true, device)?; + self.write_bits_to_gpio(i2c, i2c_address)?; + self.set_enable(false, device)?; + self.write_bits_to_gpio(i2c, i2c_address)?; + Ok(()) } + /// read bytes from the indicated device. The size of the buffer is the number of bytes to read. + /// What is read depends on the `rs_setting` parameter. If `rs_setting` is `true`, the data is read + /// from the data register, either the CGRAM or DDRAM, depending on prior command sent. If `rs_setting` + /// is `false`, the data is read from the busy flag and address register. + /// Note that while nothing "breaks" passing a buffer size greater than one when `rs_setting` is `false`, + /// the data returned will be the same for each byte read. + fn read_bytes_from_device( + &self, + _i2c: &mut I2C, + _i2c_address: u8, + _device: usize, + _rs_setting: bool, + _buffer: &mut [u8], + ) -> Result<(), AdapterError> { + unimplemented!("Reads are not supported for device"); + } + + fn is_busy(&self, _i2c: &mut I2C, _i2c_address: u8) -> Result> { + Ok(false) + } + fn device_count(&self) -> usize { 1 } diff --git a/src/adapter_config/adafruit_lcd_backpack.rs b/src/adapter_config/adafruit_lcd_backpack.rs index 8997c82..ff3de66 100644 --- a/src/adapter_config/adafruit_lcd_backpack.rs +++ b/src/adapter_config/adafruit_lcd_backpack.rs @@ -44,6 +44,10 @@ where 0x20 } + fn supports_reads() -> bool { + false + } + fn set_rs(&mut self, value: bool) { self.bits.set_rs(value as u8); } @@ -53,7 +57,7 @@ where // Not used } - fn set_enable(&mut self, value: bool, _device: usize) -> Result<(), AdapterError> { + fn set_enable(&mut self, value: bool, _device: usize) -> Result<(), AdapterError> { self.bits.set_enable(value as u8); Ok(()) } @@ -72,10 +76,11 @@ where Ok(()) } - fn write_bits_to_gpio(&self, i2c: &mut I2C, i2c_address: u8) -> Result<(), I2C::Error> { + fn write_bits_to_gpio(&self, i2c: &mut I2C, i2c_address: u8) -> Result<(), AdapterError> { // first byte is GPIO register address let data = [0x09, self.bits.0]; - i2c.write(i2c_address, &data)?; + i2c.write(i2c_address, &data) + .map_err(AdapterError::I2CError)?; Ok(()) } diff --git a/src/adapter_config/dual_hd44780.rs b/src/adapter_config/dual_hd44780.rs index 193d39f..367fccd 100644 --- a/src/adapter_config/dual_hd44780.rs +++ b/src/adapter_config/dual_hd44780.rs @@ -15,6 +15,12 @@ bitfield! { pub data, set_data: 7, 4; } +impl Clone for DualHD44780_PCF8574TBitField { + fn clone(&self) -> Self { + Self(self.0) + } +} + pub struct DualHD44780_PCF8574TConfig { bits: DualHD44780_PCF8574TBitField, _marker: PhantomData, @@ -44,6 +50,10 @@ where 0x27 } + fn supports_reads() -> bool { + false + } + fn set_rs(&mut self, value: bool) { self.bits.set_rs(value as u8); } @@ -53,7 +63,7 @@ where // does nothing } - fn set_enable(&mut self, value: bool, device: usize) -> Result<(), AdapterError> { + 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 { @@ -98,7 +108,7 @@ mod tests { #[test] fn test_bad_device_id() { let mut config = DualHD44780_PCF8574TConfig::::default(); - assert_eq!(config.set_enable(true, 2), Err(AdapterError::BadDeviceId)); + assert!(config.set_enable(true, 2).is_err()); } #[test] diff --git a/src/adapter_config/generic_pcf8574t.rs b/src/adapter_config/generic_pcf8574t.rs index 48a1a2f..f5eb2e6 100644 --- a/src/adapter_config/generic_pcf8574t.rs +++ b/src/adapter_config/generic_pcf8574t.rs @@ -18,6 +18,13 @@ bitfield! { pub data, set_data: 7, 4; } +impl Clone for GenericPCF8574TBitField { + fn clone(&self) -> Self { + Self(self.0) + } +} + +#[derive(Clone)] pub struct GenericPCF8574TConfig { bits: GenericPCF8574TBitField, _marker: PhantomData, @@ -47,6 +54,94 @@ where 0x27 } + fn supports_reads() -> bool { + true + } + + fn read_bytes_from_device( + &self, + i2c: &mut I2C, + i2c_address: u8, + _device: usize, + rs_setting: bool, + buffer: &mut [u8], + ) -> Result<(), AdapterError> { + // wait for the BUSY flag to clear + while self.is_busy(i2c, i2c_address)? { + // wait + } + + // now we can read the data. Set up PCF8574T to read data + let mut data_cntl = self.bits.clone(); + data_cntl.set_data(0b1111); + data_cntl.set_enable(0); + data_cntl.set_rs(rs_setting as u8); + data_cntl.set_rw(1); // read + i2c.write(i2c_address, &[data_cntl.0]) + .map_err(AdapterError::I2CError)?; + + // not that is is set up, read bytes into buffer + let mut data_buf = [0]; + for byte in buffer { + *byte = 0; + // read high nibble + data_cntl.set_enable(1); + i2c.write(i2c_address, &[data_cntl.0]) + .map_err(AdapterError::I2CError)?; + i2c.read(i2c_address, &mut data_buf) + .map_err(AdapterError::I2CError)?; + data_cntl.set_enable(0); + i2c.write(i2c_address, &[data_cntl.0]) + .map_err(AdapterError::I2CError)?; + *byte = GenericPCF8574TBitField(data_buf[0]).data() << 4; + + // read low nibble + data_cntl.set_enable(1); + i2c.write(i2c_address, &[data_cntl.0]) + .map_err(AdapterError::I2CError)?; + i2c.read(i2c_address, &mut data_buf) + .map_err(AdapterError::I2CError)?; + data_cntl.set_enable(0); + i2c.write(i2c_address, &[data_cntl.0]) + .map_err(AdapterError::I2CError)?; + *byte |= GenericPCF8574TBitField(data_buf[0]).data() & 0x0F; + } + Ok(()) + } + + fn is_busy(&self, i2c: &mut I2C, i2c_address: u8) -> Result> { + // need to set all data bits to HIGH to read, per PFC8574 data sheet description of Quasi-bidirectional I/Os + let mut setup = self.bits.clone(); + setup.set_data(0b1111); + setup.set_rs(0); + setup.set_rw(1); + setup.set_enable(0); + i2c.write(i2c_address, &[setup.0]) + .map_err(AdapterError::I2CError)?; + // need two enable cycles to read the data, but the busy flag is in the 4th bit of the first + // nibble, so we only need to read the first nibble + setup.set_enable(1); + i2c.write(i2c_address, &[setup.0]) + .map_err(AdapterError::I2CError)?; + let mut data = [0]; + i2c.read(i2c_address, &mut data) + .map_err(AdapterError::I2CError)?; + let read_data = GenericPCF8574TBitField(data[0]); + // turn off the enable bit so next nibble can be read + setup.set_enable(0); + i2c.write(i2c_address, &[setup.0]) + .map_err(AdapterError::I2CError)?; + // toggle enable one more time per the 4-bit interface for the HD44780 + setup.set_enable(1); + i2c.write(i2c_address, &[setup.0]) + .map_err(AdapterError::I2CError)?; + setup.set_enable(0); + i2c.write(i2c_address, &[setup.0]) + .map_err(AdapterError::I2CError)?; + + Ok(read_data.data() & 0b1000 != 0) + } + fn set_rs(&mut self, value: bool) { self.bits.set_rs(value as u8); } @@ -55,7 +150,7 @@ where self.bits.set_rw(value as u8); } - fn set_enable(&mut self, value: bool, _device: usize) -> Result<(), AdapterError> { + fn set_enable(&mut self, value: bool, _device: usize) -> Result<(), AdapterError> { self.bits.set_enable(value as u8); Ok(()) } @@ -121,7 +216,7 @@ mod tests { } #[test] - fn test_generic_pcf8574t_config_write_bits_to_gpio() { + fn test_generic_pcf8574t_config_write_byte() { let mut config = GenericPCF8574TConfig::::default(); config.set_rs(true); config.set_rw(false); @@ -135,4 +230,104 @@ mod tests { config.write_bits_to_gpio(&mut i2c, 0x27).unwrap(); i2c.done(); } + + #[test] + fn test_generic_pcf8574t_config_read_bytes() { + let expected_transactions = [ + // set up PCF8574T to read data for is busy check - true + I2cTransaction::write(0x27, std::vec![0b11110010]), + // read high nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::read(0x27, std::vec![0b10100110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + // read low nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + // set up PCF8574T to read data for is busy check - false + I2cTransaction::write(0x27, std::vec![0b11110010]), + // read high nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::read(0x27, std::vec![0b00100110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + // read low nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + // set up PCF8574T to read data for data read + I2cTransaction::write(0x27, std::vec![0b11110010]), + // Byte 0 = $DE + // read high nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::read(0x27, std::vec![0b11010110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + // read low nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::read(0x27, std::vec![0b11100110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + // Byte 0 = $AD + // read high nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::read(0x27, std::vec![0b10100110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + // read low nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::read(0x27, std::vec![0b11010110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + ]; + let mut i2c = I2cMock::new(&expected_transactions); + + let config = GenericPCF8574TConfig::::default(); + + let buffer = &mut [0u8; 2]; + assert!(config + .read_bytes_from_device(&mut i2c, 0x27, 0, false, buffer) + .is_ok()); + assert_eq!(buffer, &[0xDE, 0xAD]); + i2c.done(); + } + + #[test] + fn test_generic_pcf8574t_config_is_busy() { + let expected_transactions = [ + // set up PCF8574T to read data + I2cTransaction::write(0x27, std::vec![0b11110010]), + // read high nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::read(0x27, std::vec![0b10100110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + // read low nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + ]; + let mut i2c = I2cMock::new(&expected_transactions); + + let config = GenericPCF8574TConfig::::default(); + + let is_busy = config.is_busy(&mut i2c, 0x27).unwrap(); + + assert_eq!(is_busy, true); + i2c.done(); + } + + #[test] + fn test_generic_pcf8574t_config_is_not_busy() { + let expected_transactions = [ + // set up PCF8574T to read data + I2cTransaction::write(0x27, std::vec![0b11110010]), + // read high nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::read(0x27, std::vec![0b00100110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + // read low nibble + I2cTransaction::write(0x27, std::vec![0b11110110]), + I2cTransaction::write(0x27, std::vec![0b11110010]), + ]; + let mut i2c = I2cMock::new(&expected_transactions); + + let config = GenericPCF8574TConfig::::default(); + + let is_busy = config.is_busy(&mut i2c, 0x27).unwrap(); + + assert_eq!(is_busy, false); + i2c.done(); + } } diff --git a/src/lib.rs b/src/lib.rs index 4a48fdc..ca1dc4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,8 @@ //! - `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 uses multiple HD44780 drivers, such as the 40x4 display +//! - Optional support for the `defmt` and `ufmt` logging frameworks +//! - Optional support for reading from the display on adapters that support it //! //! ## Usage //! Add this to your `Cargo.toml`: @@ -78,6 +80,11 @@ //! ```rust //! lcd.backlight(true)?.clear()?.home()?.print("Hello, world!")?; //! ``` +//! ### Reading from the display +//! Some I2C adapters support reading data from the HD44780 controller. Dor the I2C adapters that support it, the `read_device_data` method can be used to read +//! from either the CGRAM or DDRAM at the current cursor position. The `read_address_counter` method can be used to read the address counter from the HD44780 controller. +//! In both cases, the specific meaning of the data depends on the prior commands sent to the display. See the HD44780 datasheet for more information. +//! //! ### Multiple HD44780 controller character displays //! Some character displays, such as the 40x4 display, use two HD44780 controllers to drive the display. This library supports these displays by //! treating them as one logical display with multiple HD44780 controllers. The `CharacterDisplayDualHD44780` type is used to control these displays. @@ -86,6 +93,8 @@ //! #![no_std] #![allow(dead_code, non_camel_case_types, non_upper_case_globals)] +use core::fmt::Display; + use embedded_hal::{delay::DelayNs, i2c}; // commands @@ -145,9 +154,11 @@ where /// Formatting error FormattingError(core::fmt::Error), /// Dive Adapter Error - AdapterError(adapter_config::AdapterError), + AdapterError(adapter_config::AdapterError), /// The discplay type is not compatible with specific adapter. UnsupportedDisplayType, + /// Read operation is not supported by the adapter + ReadNotSupported, } impl From for Error @@ -159,12 +170,35 @@ where } } -impl From for Error +impl From> for Error where I2C: i2c::I2c, { - fn from(err: adapter_config::AdapterError) -> Self { - Error::AdapterError(err) + fn from(err: adapter_config::AdapterError) -> Self { + match err { + adapter_config::AdapterError::I2CError(i2c_err) => Error::I2cError(i2c_err), + _ => Error::AdapterError(err), + } + } +} + +impl From<&Error> for &'static str +where + I2C: i2c::I2c, +{ + fn from(err: &Error) -> Self { + match err { + Error::I2cError(_) => "I2C error", + Error::RowOutOfRange => "Row out of range", + Error::ColumnOutOfRange => "Column out of range", + Error::FormattingError(_) => "Formatting error", + Error::AdapterError(e) => match e { + adapter_config::AdapterError::BadDeviceId => "Bad device ID", + adapter_config::AdapterError::I2CError(_) => "Adapter I2C error", + }, + Error::UnsupportedDisplayType => "Unsupported display type", + Error::ReadNotSupported => "Read operation not supported", + } } } @@ -174,14 +208,8 @@ where I2C: i2c::I2c, { fn format(&self, fmt: defmt::Formatter) { - match self { - Error::I2cError(_e) => defmt::write!(fmt, "I2C error"), - 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"), - } + let msg: &'static str = From::from(self); + defmt::write!(fmt, "{}", msg); } } @@ -194,14 +222,18 @@ where where W: ufmt::uWrite + ?Sized, { - match self { - Error::I2cError(_e) => ufmt::uwrite!(w, "I2C error"), - Error::RowOutOfRange => ufmt::uwrite!(w, "Row out of range"), - Error::ColumnOutOfRange => ufmt::uwrite!(w, "Column out of range"), - Error::FormattingError(_e) => ufmt::uwrite!(w, "Formatting error"), - Error::AdapterError(e) => ufmt::uwrite!(w, "Adapter error: {}", e), - Error::UnsupportedDisplayType => ufmt::uwrite!(w, "Unsupported display type"), - } + let msg: &'static str = From::from(self); + ufmt::uwrite!(w, "{}", msg) + } +} + +impl Display for Error +where + I2C: i2c::I2c, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let msg: &'static str = From::from(self); + write!(f, "{}", msg) } } @@ -224,18 +256,25 @@ pub enum LcdDisplayType { Lcd40x4, } +impl From<&LcdDisplayType> for &'static str { + fn from(display_type: &LcdDisplayType) -> Self { + match display_type { + LcdDisplayType::Lcd20x4 => "20x4", + LcdDisplayType::Lcd20x2 => "20x2", + LcdDisplayType::Lcd16x2 => "16x2", + LcdDisplayType::Lcd16x4 => "16x4", + LcdDisplayType::Lcd8x2 => "8x2", + LcdDisplayType::Lcd40x2 => "40x2", + LcdDisplayType::Lcd40x4 => "40x4", + } + } +} + #[cfg(feature = "defmt")] impl defmt::Format for LcdDisplayType { fn format(&self, fmt: defmt::Formatter) { - match self { - LcdDisplayType::Lcd20x4 => defmt::write!(fmt, "20x4"), - LcdDisplayType::Lcd20x2 => defmt::write!(fmt, "20x2"), - LcdDisplayType::Lcd16x2 => defmt::write!(fmt, "16x2"), - LcdDisplayType::Lcd16x4 => defmt::write!(fmt, "16x4"), - LcdDisplayType::Lcd8x2 => defmt::write!(fmt, "8x2"), - LcdDisplayType::Lcd40x2 => defmt::write!(fmt, "40x2"), - LcdDisplayType::Lcd40x4 => defmt::write!(fmt, "40x4"), - } + let msg: &'static str = From::from(self); + defmt::write!(fmt, "{}", msg); } } @@ -245,15 +284,15 @@ impl ufmt::uDisplay for LcdDisplayType { where W: ufmt::uWrite + ?Sized, { - match self { - LcdDisplayType::Lcd20x4 => ufmt::uwrite!(w, "20x4"), - LcdDisplayType::Lcd20x2 => ufmt::uwrite!(w, "20x2"), - LcdDisplayType::Lcd16x2 => ufmt::uwrite!(w, "16x2"), - LcdDisplayType::Lcd16x4 => ufmt::uwrite!(w, "16x4"), - LcdDisplayType::Lcd8x2 => ufmt::uwrite!(w, "8x2"), - LcdDisplayType::Lcd40x2 => ufmt::uwrite!(w, "40x2"), - LcdDisplayType::Lcd40x4 => ufmt::uwrite!(w, "40x4"), - } + let msg: &'static str = From::from(self); + ufmt::uwrite!(w, "{}", msg) + } +} + +impl Display for LcdDisplayType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let msg: &'static str = From::from(self); + write!(f, "{}", msg) } } @@ -381,16 +420,21 @@ where 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.device + .write_nibble_to_device(&mut self.i2c, self.address, device, false, 0x03)?; self.delay.delay_ms(5); - self.write_4_bits(0x03, device)?; + self.device + .write_nibble_to_device(&mut self.i2c, self.address, device, false, 0x03)?; self.delay.delay_ms(5); - self.write_4_bits(0x03, device)?; + self.device + .write_nibble_to_device(&mut self.i2c, self.address, device, false, 0x03)?; self.delay.delay_us(150); - self.write_4_bits(0x02, device)?; + self.device + .write_nibble_to_device(&mut self.i2c, self.address, device, false, 0x02)?; self.send_command_to_device( LCD_CMD_FUNCTIONSET | self.display_function[device], @@ -419,6 +463,11 @@ where self.lcd_type } + /// Supports the ability to read from the display. + pub fn supports_reads() -> bool { + DEVICE::supports_reads() + } + /// Sends a command datum to the display. Normally users do not need to call this directly. /// For multiple devices, this sends the command to the currently active contoller device. fn send_command(&mut self, command: u8) -> Result<(), Error> { @@ -427,9 +476,9 @@ where /// Sends a command datum to a specific HD44780 controller device. Normally users do not need to call this directly. 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(()) + self.device + .write_byte_to_device(&mut self.i2c, self.address, device, false, command) + .map_err(Error::AdapterError) } /// Writes a data byte to the display. Normally users do not need to call this directly. @@ -440,31 +489,47 @@ where /// Writes a data byte to a specific HD44780 controller device. Normally users do not need to call this directly. 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(()) + self.device + .write_byte_to_device(&mut self.i2c, self.address, device, true, data) + .map_err(Error::AdapterError) } - 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)?; + /// Reads into the buffer data from the display device either the CGRAM or DDRAM at the current cursor position. + /// For multiple devices, this reads from the currently active device as set by the cursor position. + /// The amount of data read is determined by the length of the buffer. + /// Not all adapters support reads from the device. This will return an error if the adapter + /// does not support reads. + pub fn read_device_data(&mut self, buffer: &mut [u8]) -> Result<(), Error> { + if !DEVICE::supports_reads() { + return Err(Error::ReadNotSupported); + } + self.device.read_bytes_from_device( + &mut self.i2c, + self.address, + self.active_device, + true, + buffer, + )?; Ok(()) } - 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.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); - Ok(()) + /// Reads the address counter from the display device. The ready bit is masked off. + /// Not all adapters support reads from the device. This will return an error if the adapter + /// does not support reads. + pub fn read_address_counter(&mut self) -> Result> { + if !DEVICE::supports_reads() { + return Err(Error::ReadNotSupported); + } + let mut buffer = [0]; + self.device.read_bytes_from_device( + &mut self.i2c, + self.address, + self.active_device, + false, + &mut buffer, + )?; + // mask off the busy flag + Ok(buffer[0] & 0x7F) } //-------------------------------------------------------------------------------------------------- @@ -730,9 +795,9 @@ where /// Turn the backlight on or off pub fn backlight(&mut self, on: bool) -> Result<&mut Self, Error> { self.device.set_backlight(on); + self.device.set_enable(false, self.active_device)?; self.device - .write_bits_to_gpio(&mut self.i2c, self.address) - .map_err(Error::I2cError)?; + .write_bits_to_gpio(&mut self.i2c, self.address)?; Ok(self) } } @@ -799,7 +864,6 @@ mod lib_tests { // 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