1use 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#[derive(Clone, Debug)]
18pub(crate) enum CharLcdMessage {
19 Display {
21 text: String<64>, duration_ms: u32,
23 },
24}
25
26pub struct CharLcdStatic(Channel<CriticalSectionRawMutex, CharLcdMessage, 8>);
28
29impl CharLcdStatic {
30 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
44pub struct CharLcd {
64 char_lcd_static: &'static CharLcdStatic,
65}
66
67impl CharLcd {
68 #[must_use]
70 pub const fn new_static() -> CharLcdStatic {
71 CharLcdStatic::new()
72 }
73
74 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 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 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
104struct LcdDriver {
106 i2c: i2c::I2c<'static, I2C0, i2c::Blocking>,
107 address: u8,
108}
109
110const 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 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 self.write_byte_internal(0x28, false).await;
133 self.write_byte_internal(0x0C, false).await;
135 self.write_byte_internal(0x01, false).await;
137 Timer::after_millis(2).await;
138 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 self.i2c
149 .blocking_write(self.address, &[data | LCD_ENABLE])
150 .ok();
151 Timer::after_micros(1).await;
152
153 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 lcd.clear().await;
201
202 let text_str = text.as_str();
204 if let Some(newline_pos) = text_str.find('\n') {
205 let (line1, rest) = text_str.split_at(newline_pos);
207 let line2 = &rest[1..]; lcd.print(line1).await;
211 lcd.set_cursor(1, 0).await;
213 lcd.print(line2).await;
215 } else {
216 lcd.print(text_str).await;
218 }
219
220 if duration_ms > 0 {
222 Timer::after_millis(duration_ms.into()).await;
223 }
224 }
225 }
226 }
227}