Skip to content

Commit

Permalink
Added support for reading data from character display (#3)
Browse files Browse the repository at this point in the history
Some I2C adaptors for character displays support reading data from the display. Specifically, the generic PCF8754T adaptor. This updated refactors code in order to support reading data from the display when the adaptor supports it.
  • Loading branch information
michaelkamprath authored Nov 3, 2024
1 parent a22aa11 commit 0e515ba
Show file tree
Hide file tree
Showing 8 changed files with 477 additions and 82 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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.
Expand Down
122 changes: 116 additions & 6 deletions src/adapter_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<I2C>
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<I2C> defmt::Format for AdapterError<I2C>
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<I2C> ufmt::uDisplay for AdapterError<I2C>
where
I2C: i2c::I2c,
{
fn fmt<W>(&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<I2C> Display for AdapterError<I2C>
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"),
}
}
}
Expand All @@ -36,28 +63,111 @@ pub trait AdapterConfigTrait<I2C>: 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<I2C>>;

/// 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<I2C>> {
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<I2C>> {
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<I2C>> {
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<I2C>> {
unimplemented!("Reads are not supported for device");
}

fn is_busy(&self, _i2c: &mut I2C, _i2c_address: u8) -> Result<bool, AdapterError<I2C>> {
Ok(false)
}

fn device_count(&self) -> usize {
1
}
Expand Down
11 changes: 8 additions & 3 deletions src/adapter_config/adafruit_lcd_backpack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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<I2C>> {
self.bits.set_enable(value as u8);
Ok(())
}
Expand All @@ -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<I2C>> {
// 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(())
}

Expand Down
14 changes: 12 additions & 2 deletions src/adapter_config/dual_hd44780.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<I2C> {
bits: DualHD44780_PCF8574TBitField,
_marker: PhantomData<I2C>,
Expand Down Expand Up @@ -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);
}
Expand All @@ -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<I2C>> {
if device == 0 {
self.bits.set_enable1(value as u8);
} else if device == 1 {
Expand Down Expand Up @@ -98,7 +108,7 @@ mod tests {
#[test]
fn test_bad_device_id() {
let mut config = DualHD44780_PCF8574TConfig::<I2cMock>::default();
assert_eq!(config.set_enable(true, 2), Err(AdapterError::BadDeviceId));
assert!(config.set_enable(true, 2).is_err());
}

#[test]
Expand Down
Loading

0 comments on commit 0e515ba

Please sign in to comment.