Skip to main content

device_envoy/
char_lcd.rs

1//! A device abstraction for HD44780-compatible character LCDs (e.g., 16x2, 20x2, 20x4).
2//!
3//! See [`CharLcd`] for the primary usage example.
4
5use embassy_executor::Spawner;
6use embassy_rp::Peri;
7use embassy_rp::i2c::{self, Config as I2cConfig, SclPin, SdaPin};
8use embassy_rp::peripherals::I2C0;
9use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
10use embassy_sync::channel::Channel;
11use embassy_time::Timer;
12use heapless::String;
13
14use crate::{Error, Result};
15
16/// Messages sent to the character LCD device.
17#[derive(Clone, Debug)]
18pub(crate) enum CharLcdMessage {
19    /// Display a message for the specified duration (0 = until next message)
20    Display {
21        text: String<64>, // 64 chars supports up to 20x4 displays (80 chars)
22        duration_ms: u32,
23    },
24}
25
26/// Static type for the `CharLcd` device abstraction.
27pub struct CharLcdStatic(Channel<CriticalSectionRawMutex, CharLcdMessage, 8>);
28
29impl CharLcdStatic {
30    /// Creates static resources for the character LCD device.
31    pub(crate) const fn new() -> Self {
32        Self(Channel::new())
33    }
34
35    async fn send(&self, message: CharLcdMessage) {
36        self.0.send(message).await;
37    }
38
39    async fn receive(&self) -> CharLcdMessage {
40        self.0.receive().await
41    }
42}
43
44/// A device abstraction for an HD44780-compatible character LCD.
45///
46/// ```rust,no_run
47/// # #![no_std]
48/// # use panic_probe as _;
49/// # fn main() {}
50/// use device_envoy::char_lcd::{CharLcd, CharLcdStatic};
51///
52/// async fn example(
53///     p: embassy_rp::Peripherals,
54///     spawner: embassy_executor::Spawner,
55/// ) -> device_envoy::Result<()> {
56///     static CHAR_LCD_STATIC: CharLcdStatic = CharLcd::new_static();
57///     let lcd = CharLcd::new(&CHAR_LCD_STATIC, p.I2C0, p.PIN_1, p.PIN_0, spawner)?;
58///     let mut text: heapless::String<64> = "Hello!".try_into().unwrap();
59///     lcd.write_text(text, 1_000).await;
60///     Ok(())
61/// }
62/// ```
63pub struct CharLcd {
64    char_lcd_static: &'static CharLcdStatic,
65}
66
67impl CharLcd {
68    /// Create CharLcd resources
69    #[must_use]
70    pub const fn new_static() -> CharLcdStatic {
71        CharLcdStatic::new()
72    }
73
74    /// Create a new CharLcd device
75    ///
76    /// Note: Hardcoded to I2C0 peripheral (like WiFi's internal pins).
77    /// However, SCL and SDA can be any pins compatible with I2C0.
78    pub fn new<SCL, SDA>(
79        char_lcd_static: &'static CharLcdStatic,
80        i2c_peripheral: Peri<'static, I2C0>,
81        scl: Peri<'static, SCL>,
82        sda: Peri<'static, SDA>,
83        spawner: Spawner,
84    ) -> Result<Self>
85    where
86        SCL: SclPin<I2C0>,
87        SDA: SdaPin<I2C0>,
88    {
89        // Create the I2C instance and pass it to the task
90        let i2c = i2c::I2c::new_blocking(i2c_peripheral, scl, sda, I2cConfig::default());
91        let token = lcd_task(i2c, char_lcd_static);
92        spawner.spawn(token).map_err(Error::TaskSpawn)?;
93        Ok(Self { char_lcd_static })
94    }
95
96    /// Send a message to the LCD (async, waits until queued)
97    pub async fn write_text(&self, text: String<64>, duration_ms: u32) {
98        self.char_lcd_static
99            .send(CharLcdMessage::Display { text, duration_ms })
100            .await;
101    }
102}
103
104// Internal LCD driver implementation (used by the background task)
105struct LcdDriver {
106    i2c: i2c::I2c<'static, I2C0, i2c::Blocking>,
107    address: u8,
108}
109
110// PCF8574 pin mapping: P0=RS, P1=RW, P2=E, P3=Backlight, P4-P7=Data
111const LCD_BACKLIGHT: u8 = 0x08;
112const LCD_ENABLE: u8 = 0x04;
113const LCD_RS: u8 = 0x01;
114
115impl LcdDriver {
116    fn new(i2c: i2c::I2c<'static, I2C0, i2c::Blocking>) -> Self {
117        Self { i2c, address: 0x27 }
118    }
119
120    async fn init(&mut self) {
121        Timer::after_millis(50).await;
122
123        // Initialize in 4-bit mode
124        self.write_nibble(0x03, false).await;
125        Timer::after_millis(5).await;
126        self.write_nibble(0x03, false).await;
127        Timer::after_micros(150).await;
128        self.write_nibble(0x03, false).await;
129        self.write_nibble(0x02, false).await;
130
131        // Function set: 4-bit, 2 lines, 5x8 font
132        self.write_byte_internal(0x28, false).await;
133        // Display control: display on, cursor off, blink off
134        self.write_byte_internal(0x0C, false).await;
135        // Clear display
136        self.write_byte_internal(0x01, false).await;
137        Timer::after_millis(2).await;
138        // Entry mode: increment cursor, no shift
139        self.write_byte_internal(0x06, false).await;
140    }
141
142    #[expect(clippy::arithmetic_side_effects, reason = "Bit operations")]
143    async fn write_nibble(&mut self, nibble: u8, rs: bool) {
144        let rs_bit = if rs { LCD_RS } else { 0 };
145        let data = (nibble << 4) | LCD_BACKLIGHT | rs_bit;
146
147        // Write with enable high
148        self.i2c
149            .blocking_write(self.address, &[data | LCD_ENABLE])
150            .ok();
151        Timer::after_micros(1).await;
152
153        // Write with enable low
154        self.i2c.blocking_write(self.address, &[data]).ok();
155        Timer::after_micros(50).await;
156    }
157
158    async fn write_byte_internal(&mut self, byte: u8, rs: bool) {
159        self.write_nibble((byte >> 4) & 0x0F, rs).await;
160        self.write_nibble(byte & 0x0F, rs).await;
161    }
162
163    async fn clear(&mut self) {
164        self.write_byte_internal(0x01, false).await;
165        Timer::after_millis(2).await;
166    }
167
168    #[expect(clippy::arithmetic_side_effects, reason = "Row/col values are small")]
169    async fn set_cursor(&mut self, row: u8, col: u8) {
170        let address = match row {
171            0 => 0x00 + col,
172            1 => 0x40 + col,
173            2 => 0x14 + col,
174            3 => 0x54 + col,
175            _ => 0x00,
176        };
177        self.write_byte_internal(0x80 | address, false).await;
178    }
179
180    async fn print(&mut self, s: &str) {
181        for ch in s.bytes() {
182            self.write_byte_internal(ch, true).await;
183        }
184    }
185}
186
187#[embassy_executor::task]
188async fn lcd_task(
189    i2c: i2c::I2c<'static, I2C0, i2c::Blocking>,
190    commands: &'static CharLcdStatic,
191) -> ! {
192    let mut lcd = LcdDriver::new(i2c);
193    lcd.init().await;
194
195    loop {
196        let msg = commands.receive().await;
197        match msg {
198            CharLcdMessage::Display { text, duration_ms } => {
199                // Clear and display the text
200                lcd.clear().await;
201
202                // Split text by newline and display on separate lines
203                let text_str = text.as_str();
204                if let Some(newline_pos) = text_str.find('\n') {
205                    // Two-line display
206                    let (line1, rest) = text_str.split_at(newline_pos);
207                    let line2 = &rest[1..]; // Skip the \n character
208
209                    // Display line 1
210                    lcd.print(line1).await;
211                    // Move to line 2
212                    lcd.set_cursor(1, 0).await;
213                    // Display line 2
214                    lcd.print(line2).await;
215                } else {
216                    // Single-line display
217                    lcd.print(text_str).await;
218                }
219
220                // Wait for the minimum display duration
221                if duration_ms > 0 {
222                    Timer::after_millis(duration_ms.into()).await;
223                }
224            }
225        }
226    }
227}