endbasic_rpi/
st7735s.rs

1// EndBASIC
2// Copyright 2024 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16/***************************************************************************************************
17* | file        :    LCD_Driver.c
18* | version     :    V1.0
19* | date        :    2017-10-16
20* | function    :    On the ST7735S chip driver and clear screen, drawing lines, drawing, writing
21                     and other functions to achieve
22***************************************************************************************************/
23
24//! Console driver for the ST7735S LCD.
25
26use crate::gpio::gpio_error_to_io_error;
27use crate::lcd::{to_xy_size, BufferedLcd, Lcd, LcdSize, LcdXY, RGB565Pixel};
28use async_channel::Sender;
29use async_trait::async_trait;
30use endbasic_core::exec::Signal;
31use endbasic_std::console::graphics::InputOps;
32use endbasic_std::console::{
33    CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, SizeInPixels, RGB,
34};
35use endbasic_terminal::TerminalConsole;
36use rppal::gpio::{Gpio, InputPin, Level, OutputPin};
37use rppal::spi::{self, Bus, SlaveSelect, Spi};
38use std::path::Path;
39use std::time::Duration;
40use std::{fs, io};
41
42/// Path to the configuration file containing the maximum SPI transfer size.
43const SPIDEV_BUFSIZ_PATH: &str = "/sys/module/spidev/parameters/bufsiz";
44
45/// Converts an SPI error to an IO error.
46fn spi_error_to_io_error(e: spi::Error) -> io::Error {
47    match e {
48        spi::Error::Io(e) => e,
49        e => io::Error::new(io::ErrorKind::InvalidInput, e.to_string()),
50    }
51}
52
53/// Queries the maximum SPI transfer size from `path`.  If `path` is not provided, defaults to
54/// `SPIDEV_BUFSIZ_PATH`.
55fn query_spi_bufsiz(path: Option<&Path>) -> io::Result<usize> {
56    let path = path.unwrap_or(Path::new(SPIDEV_BUFSIZ_PATH));
57
58    let content = match fs::read_to_string(path) {
59        Ok(content) => content,
60        Err(e) => {
61            return Err(io::Error::new(
62                e.kind(),
63                format!("Failed to read {}: {}", path.display(), e),
64            ));
65        }
66    };
67
68    let content = content.trim_end();
69
70    match content.parse::<usize>() {
71        Ok(i) => Ok(i),
72        Err(e) => Err(io::Error::new(
73            io::ErrorKind::InvalidData,
74            format!("Failed to read {}: invalid content: {}", path.display(), e),
75        )),
76    }
77}
78
79/// Input handler for the ST7735S console.
80///
81/// This relies on the usual terminal console in raw mode to gather keyboard input but also adds
82/// support for the physical buttons that come along with the display.
83struct ST7735SInput {
84    terminal: TerminalConsole,
85}
86
87impl ST7735SInput {
88    fn new(gpio: &mut Gpio, signals_tx: Sender<Signal>) -> io::Result<Self> {
89        let (terminal, on_key_tx) = TerminalConsole::from_stdio_with_injector(signals_tx)?;
90
91        let key_up = gpio.get(6).map_err(gpio_error_to_io_error)?.into_input_pullup();
92        let key_down = gpio.get(19).map_err(gpio_error_to_io_error)?.into_input_pullup();
93        let key_left = gpio.get(5).map_err(gpio_error_to_io_error)?.into_input_pullup();
94        let key_right = gpio.get(26).map_err(gpio_error_to_io_error)?.into_input_pullup();
95        let key_press = gpio.get(13).map_err(gpio_error_to_io_error)?.into_input_pullup();
96        let key_1 = gpio.get(21).map_err(gpio_error_to_io_error)?.into_input_pullup();
97        let key_2 = gpio.get(20).map_err(gpio_error_to_io_error)?.into_input_pullup();
98        let key_3 = gpio.get(16).map_err(gpio_error_to_io_error)?.into_input_pullup();
99
100        tokio::task::spawn(async move {
101            async fn read_button(pin: &InputPin, key: Key, tx: &Sender<Key>) {
102                if pin.read() == Level::Low {
103                    if let Err(e) = tx.send(key.clone()).await {
104                        eprintln!("Ignoring button {:?} due to error: {}", key, e);
105                    }
106                }
107            }
108
109            loop {
110                read_button(&key_up, Key::ArrowUp, &on_key_tx).await;
111                read_button(&key_down, Key::ArrowDown, &on_key_tx).await;
112                read_button(&key_left, Key::ArrowLeft, &on_key_tx).await;
113                read_button(&key_right, Key::ArrowRight, &on_key_tx).await;
114                read_button(&key_press, Key::NewLine, &on_key_tx).await;
115                read_button(&key_1, Key::Char('1'), &on_key_tx).await;
116                read_button(&key_2, Key::Char('2'), &on_key_tx).await;
117                read_button(&key_3, Key::Char('3'), &on_key_tx).await;
118
119                tokio::time::sleep(Duration::from_millis(50)).await;
120            }
121        });
122
123        Ok(Self { terminal })
124    }
125}
126
127#[async_trait(?Send)]
128impl InputOps for ST7735SInput {
129    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
130        self.terminal.poll_key().await
131    }
132
133    async fn read_key(&mut self) -> io::Result<Key> {
134        self.terminal.read_key().await
135    }
136}
137
138/// LCD handler for the ST7735S console.
139struct ST7735SLcd {
140    spi: Spi,
141    spi_bufsiz: usize,
142
143    lcd_rst: OutputPin,
144    lcd_dc: OutputPin,
145    lcd_bl: OutputPin,
146
147    size_pixels: LcdSize,
148}
149
150impl ST7735SLcd {
151    /// Initializes the LCD.
152    pub fn new(gpio: &mut Gpio) -> io::Result<Self> {
153        let mut lcd_cs = gpio.get(8).map_err(gpio_error_to_io_error)?.into_output();
154        let lcd_rst = gpio.get(27).map_err(gpio_error_to_io_error)?.into_output();
155        let lcd_dc = gpio.get(25).map_err(gpio_error_to_io_error)?.into_output();
156        let mut lcd_bl = gpio.get(24).map_err(gpio_error_to_io_error)?.into_output();
157
158        lcd_cs.write(Level::High);
159        lcd_bl.write(Level::High);
160
161        let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 9000000, spi::Mode::Mode0)
162            .map_err(spi_error_to_io_error)?;
163        spi.set_ss_polarity(spi::Polarity::ActiveLow).map_err(spi_error_to_io_error)?;
164
165        let spi_bufsiz = query_spi_bufsiz(None)?;
166
167        let size_pixels = LcdSize { width: 128, height: 128 };
168
169        let mut device = Self { spi, spi_bufsiz, lcd_rst, lcd_dc, lcd_bl, size_pixels };
170
171        device.lcd_init()?;
172
173        Ok(device)
174    }
175
176    /// Writes arbitrary data to the SPI bus.
177    ///
178    /// The input data is chunked to respect the maximum write size accepted by the SPI bus.
179    fn lcd_write(&mut self, data: &[u8]) -> io::Result<()> {
180        for chunk in data.chunks(self.spi_bufsiz) {
181            let mut i = 0;
182            loop {
183                let n = self.spi.write(&chunk[i..]).map_err(spi_error_to_io_error)?;
184                if n == 0 {
185                    break;
186                }
187                i += n;
188            }
189        }
190        Ok(())
191    }
192
193    /// Selects the registers to affect by the next data write.
194    fn lcd_write_reg(&mut self, regs: &[u8]) -> io::Result<()> {
195        self.lcd_dc.write(Level::Low);
196        self.lcd_write(regs)
197    }
198
199    /// Writes data to the device.  A register should have been selected before.
200    fn lcd_write_data(&mut self, data: &[u8]) -> io::Result<()> {
201        self.lcd_dc.write(Level::High);
202        self.lcd_write(data)
203    }
204
205    /// Resets the LCD.
206    fn lcd_reset(&mut self) {
207        self.lcd_rst.write(Level::High);
208        std::thread::sleep(Duration::from_millis(100));
209        self.lcd_rst.write(Level::Low);
210        std::thread::sleep(Duration::from_millis(100));
211        self.lcd_rst.write(Level::High);
212        std::thread::sleep(Duration::from_millis(100));
213    }
214
215    /// Sets up the LCD registers.
216    fn lcd_init_reg(&mut self) -> io::Result<()> {
217        // ST7735R Frame Rate.
218        self.lcd_write_reg(&[0xb1])?;
219        self.lcd_write_data(&[0x01, 0x2c, 0x2d])?;
220
221        self.lcd_write_reg(&[0xb2])?;
222        self.lcd_write_data(&[0x01, 0x2c, 0x2d])?;
223
224        self.lcd_write_reg(&[0xb3])?;
225        self.lcd_write_data(&[0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d])?;
226
227        // Column inversion.
228        self.lcd_write_reg(&[0xb4])?;
229        self.lcd_write_data(&[0x07])?;
230
231        // ST7735R Power Sequence.
232        self.lcd_write_reg(&[0xc0])?;
233        self.lcd_write_data(&[0xa2, 0x02, 0x84])?;
234        self.lcd_write_reg(&[0xc1])?;
235        self.lcd_write_data(&[0xc5])?;
236
237        self.lcd_write_reg(&[0xc2])?;
238        self.lcd_write_data(&[0x0a, 0x00])?;
239
240        self.lcd_write_reg(&[0xc3])?;
241        self.lcd_write_data(&[0x8a, 0x2a])?;
242        self.lcd_write_reg(&[0xc4])?;
243        self.lcd_write_data(&[0x8a, 0xee])?;
244
245        // VCOM.
246        self.lcd_write_reg(&[0xc5])?;
247        self.lcd_write_data(&[0x0e])?;
248
249        // ST7735R Gamma Sequence.
250        self.lcd_write_reg(&[0xe0])?;
251        self.lcd_write_data(&[
252            0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f, 0x1b, 0x23, 0x37, 0x00, 0x07,
253            0x02, 0x10,
254        ])?;
255
256        self.lcd_write_reg(&[0xe1])?;
257        self.lcd_write_data(&[
258            0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30, 0x30, 0x39, 0x3f, 0x00, 0x07,
259            0x03, 0x10,
260        ])?;
261
262        // Enable test command.
263        self.lcd_write_reg(&[0xf0])?;
264        self.lcd_write_data(&[0x01])?;
265
266        // Disable ram power save mode.
267        self.lcd_write_reg(&[0xf6])?;
268        self.lcd_write_data(&[0x00])?;
269
270        // 65k mode.
271        self.lcd_write_reg(&[0x3a])?;
272        self.lcd_write_data(&[0x05])?;
273
274        Ok(())
275    }
276
277    /// Initializes the LCD scan direction and pixel color encoding.
278    fn lcd_set_gram_scan_way(&mut self) -> io::Result<()> {
279        self.lcd_write_reg(&[0x36])?; // MX, MY, RGB mode.
280        let scan_dir = 0x40 | 0x20; // X, Y.
281        let rgb_mode = 0x08; // RGB for 1.44in display.
282        self.lcd_write_data(&[scan_dir | rgb_mode])?;
283        Ok(())
284    }
285
286    /// Initializes the LCD.
287    fn lcd_init(&mut self) -> io::Result<()> {
288        self.lcd_bl.write(Level::High);
289
290        self.lcd_reset();
291        self.lcd_init_reg()?;
292
293        self.lcd_set_gram_scan_way()?;
294        std::thread::sleep(Duration::from_millis(200));
295
296        self.lcd_write_reg(&[0x11])?;
297        std::thread::sleep(Duration::from_millis(200));
298
299        // Turn display on.
300        self.lcd_write_reg(&[0x29])?;
301
302        Ok(())
303    }
304
305    /// Configures the LCD so that the next write, which carries pixel data, affects the specified
306    /// region.
307    fn lcd_set_window(&mut self, xy: LcdXY, size: LcdSize) -> io::Result<()> {
308        let adjust_x = 1;
309        let adjust_y = 2;
310
311        let x1 = ((xy.x & 0xff) + adjust_x) as u8;
312        let x2 = (((xy.x + size.width) + adjust_x - 1) & 0xff) as u8;
313        let y1 = ((xy.y & 0xff) + adjust_y) as u8;
314        let y2 = (((xy.y + size.height) + adjust_y - 1) & 0xff) as u8;
315
316        self.lcd_write_reg(&[0x2a])?;
317        self.lcd_write_data(&[0x00, x1, 0x00, x2])?;
318
319        self.lcd_write_reg(&[0x2b])?;
320        self.lcd_write_data(&[0x00, y1, 0x00, y2])?;
321
322        self.lcd_write_reg(&[0x2c])?;
323
324        Ok(())
325    }
326}
327
328impl Drop for ST7735SLcd {
329    fn drop(&mut self) {
330        self.lcd_bl.write(Level::Low);
331    }
332}
333
334impl Lcd for ST7735SLcd {
335    type Pixel = RGB565Pixel;
336
337    fn info(&self) -> (LcdSize, usize) {
338        (self.size_pixels, 2)
339    }
340
341    fn encode(&self, rgb: RGB) -> Self::Pixel {
342        let rgb = (u16::from(rgb.0), u16::from(rgb.1), u16::from(rgb.2));
343
344        let pixel: u16 = ((rgb.0 >> 3) << 11) | ((rgb.1 >> 2) << 5) | (rgb.2 >> 3);
345
346        let high = (pixel >> 8) as u8;
347        let low = (pixel & 0xff) as u8;
348        RGB565Pixel([high, low])
349    }
350
351    fn set_data(&mut self, x1y1: LcdXY, x2y2: LcdXY, data: &[u8]) -> io::Result<()> {
352        let (xy, size) = to_xy_size(x1y1, x2y2);
353        self.lcd_set_window(xy, size)?;
354        self.lcd_write_data(data)
355    }
356}
357
358/// Console implementation using a ST7735S LCD.
359pub struct ST7735SConsole {
360    /// GPIO controller used for the LCD and the input buttons.  Must be kept alive for as long as
361    /// `inner` is.
362    _gpio: Gpio,
363
364    /// The graphical console itself.  We wrap it in a struct to prevent leaking all auxiliary types
365    /// outside of this crate.
366    inner: GraphicsConsole<ST7735SInput, BufferedLcd<ST7735SLcd>>,
367}
368
369#[async_trait(?Send)]
370impl Console for ST7735SConsole {
371    fn clear(&mut self, how: ClearType) -> io::Result<()> {
372        self.inner.clear(how)
373    }
374
375    fn color(&self) -> (Option<u8>, Option<u8>) {
376        self.inner.color()
377    }
378
379    fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
380        self.inner.set_color(fg, bg)
381    }
382
383    fn enter_alt(&mut self) -> io::Result<()> {
384        self.inner.enter_alt()
385    }
386
387    fn hide_cursor(&mut self) -> io::Result<()> {
388        self.inner.hide_cursor()
389    }
390
391    fn is_interactive(&self) -> bool {
392        self.inner.is_interactive()
393    }
394
395    fn leave_alt(&mut self) -> io::Result<()> {
396        self.inner.leave_alt()
397    }
398
399    fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
400        self.inner.locate(pos)
401    }
402
403    fn move_within_line(&mut self, off: i16) -> io::Result<()> {
404        self.inner.move_within_line(off)
405    }
406
407    fn print(&mut self, text: &str) -> io::Result<()> {
408        self.inner.print(text)
409    }
410
411    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
412        self.inner.poll_key().await
413    }
414
415    async fn read_key(&mut self) -> io::Result<Key> {
416        self.inner.read_key().await
417    }
418
419    fn show_cursor(&mut self) -> io::Result<()> {
420        self.inner.show_cursor()
421    }
422
423    fn size_chars(&self) -> io::Result<CharsXY> {
424        self.inner.size_chars()
425    }
426
427    fn size_pixels(&self) -> io::Result<SizeInPixels> {
428        self.inner.size_pixels()
429    }
430
431    fn write(&mut self, text: &str) -> io::Result<()> {
432        self.inner.write(text)
433    }
434
435    fn draw_circle(&mut self, center: PixelsXY, radius: u16) -> io::Result<()> {
436        self.inner.draw_circle(center, radius)
437    }
438
439    fn draw_circle_filled(&mut self, center: PixelsXY, radius: u16) -> io::Result<()> {
440        self.inner.draw_circle_filled(center, radius)
441    }
442
443    fn draw_line(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
444        self.inner.draw_line(x1y1, x2y2)
445    }
446
447    fn draw_pixel(&mut self, xy: PixelsXY) -> io::Result<()> {
448        self.inner.draw_pixel(xy)
449    }
450
451    fn draw_rect(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
452        self.inner.draw_rect(x1y1, x2y2)
453    }
454
455    fn draw_rect_filled(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
456        self.inner.draw_rect_filled(x1y1, x2y2)
457    }
458
459    fn sync_now(&mut self) -> io::Result<()> {
460        self.inner.sync_now()
461    }
462
463    fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
464        self.inner.set_sync(enabled)
465    }
466}
467
468/// Initializes a new console on a ST7735S LCD.
469pub fn new_st7735s_console(signals_tx: Sender<Signal>) -> io::Result<ST7735SConsole> {
470    let mut gpio = Gpio::new().map_err(gpio_error_to_io_error)?;
471
472    let lcd = ST7735SLcd::new(&mut gpio)?;
473    let input = ST7735SInput::new(&mut gpio, signals_tx)?;
474    let lcd = BufferedLcd::new(lcd);
475    let inner = GraphicsConsole::new(input, lcd)?;
476    Ok(ST7735SConsole { _gpio: gpio, inner })
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    #[test]
484    fn test_query_spi_bufsiz_with_newline() {
485        let tempdir = tempfile::tempdir().unwrap();
486        let tempfile = tempdir.path().join("bufsiz");
487        fs::write(&tempfile, "1024\n").unwrap();
488        assert_eq!(1024, query_spi_bufsiz(Some(&tempfile)).unwrap());
489    }
490
491    #[test]
492    fn test_query_spi_bufsiz_without_newline() {
493        let tempdir = tempfile::tempdir().unwrap();
494        let tempfile = tempdir.path().join("bufsiz");
495        fs::write(&tempfile, "4096").unwrap();
496        assert_eq!(4096, query_spi_bufsiz(Some(&tempfile)).unwrap());
497    }
498}