├── .gitignore ├── README.md ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── src ├── instruction.rs └── lib.rs └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .idea/* 4 | *.iml 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # st7735-lcd-rs 2 | 3 | This is a Rust library for displays using the ST7735 driver with embedded_graphics, embedded_hal, and no_std, no_alloc support. Documentation is available [here](https://docs.rs/st7735-lcd). Examples are [here](https://github.com/sajattack/st7735-lcd-examples) 4 | 5 | ![ferris-demo](https://i.imgur.com/T1086fn.jpg) 6 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Install Target 12 | run: rustup target install thumbv7em-none-eabihf 13 | - uses: actions/checkout@v1 14 | - name: Build 15 | run: cargo build --release --target=thumbv7em-none-eabihf 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "st7735-lcd" 3 | description = "ST7735 TFT LCD driver with embedded-graphics support" 4 | version = "0.10.0" 5 | authors = ["Paul Sajna "] 6 | edition = "2018" 7 | license = "MIT" 8 | repository = "https://github.com/sajattack/st7735-lcd-rs" 9 | readme = "README.md" 10 | documentation = "https://docs.rs/st7735-lcd" 11 | 12 | [dependencies] 13 | embedded-hal = "1.0.0" 14 | nb = "1.1.0" 15 | 16 | [dependencies.embedded-graphics-core] 17 | version = "0.4" 18 | optional = true 19 | 20 | [features] 21 | default = ["graphics"] 22 | graphics = ["embedded-graphics-core"] 23 | -------------------------------------------------------------------------------- /src/instruction.rs: -------------------------------------------------------------------------------- 1 | /// ST7735 instructions. 2 | #[derive(Debug, Clone, Copy)] 3 | pub enum Instruction { 4 | NOP = 0x00, 5 | SWRESET = 0x01, 6 | RDDID = 0x04, 7 | RDDST = 0x09, 8 | SLPIN = 0x10, 9 | SLPOUT = 0x11, 10 | PTLON = 0x12, 11 | NORON = 0x13, 12 | INVOFF = 0x20, 13 | INVON = 0x21, 14 | DISPOFF = 0x28, 15 | DISPON = 0x29, 16 | CASET = 0x2A, 17 | RASET = 0x2B, 18 | RAMWR = 0x2C, 19 | RAMRD = 0x2E, 20 | PTLAR = 0x30, 21 | COLMOD = 0x3A, 22 | MADCTL = 0x36, 23 | FRMCTR1 = 0xB1, 24 | FRMCTR2 = 0xB2, 25 | FRMCTR3 = 0xB3, 26 | INVCTR = 0xB4, 27 | DISSET5 = 0xB6, 28 | PWCTR1 = 0xC0, 29 | PWCTR2 = 0xC1, 30 | PWCTR3 = 0xC2, 31 | PWCTR4 = 0xC3, 32 | PWCTR5 = 0xC4, 33 | VMCTR1 = 0xC5, 34 | RDID1 = 0xDA, 35 | RDID2 = 0xDB, 36 | RDID3 = 0xDC, 37 | RDID4 = 0xDD, 38 | PWCTR6 = 0xFC, 39 | GMCTRP1 = 0xE0, 40 | GMCTRN1 = 0xE1, 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 Paul Sajna 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | //! This crate provides a ST7735 driver to connect to TFT displays. 4 | 5 | pub mod instruction; 6 | 7 | use crate::instruction::Instruction; 8 | 9 | use embedded_hal::delay::DelayNs; 10 | use embedded_hal::spi; 11 | use embedded_hal::digital::OutputPin; 12 | 13 | /// ST7735 driver to connect to TFT displays. 14 | pub struct ST7735 15 | where 16 | SPI: spi::SpiDevice, 17 | DC: OutputPin, 18 | RST: OutputPin, 19 | { 20 | /// SPI 21 | spi: SPI, 22 | 23 | /// Data/command pin. 24 | dc: DC, 25 | 26 | /// Reset pin. 27 | rst: Option, 28 | 29 | /// Whether the display is RGB (true) or BGR (false) 30 | rgb: bool, 31 | 32 | /// Whether the colours are inverted (true) or not (false) 33 | inverted: bool, 34 | 35 | /// Global image offset 36 | dx: u16, 37 | dy: u16, 38 | width: u32, 39 | height: u32, 40 | } 41 | 42 | /// Display orientation. 43 | #[derive(Clone, Copy)] 44 | pub enum Orientation { 45 | Portrait = 0x00, 46 | Landscape = 0x60, 47 | PortraitSwapped = 0xC0, 48 | LandscapeSwapped = 0xA0, 49 | } 50 | 51 | impl ST7735 52 | where 53 | SPI: spi::SpiDevice, 54 | DC: OutputPin, 55 | RST: OutputPin, 56 | { 57 | /// Creates a new driver instance that uses hardware SPI. 58 | pub fn new( 59 | spi: SPI, 60 | dc: DC, 61 | rst: Option, 62 | rgb: bool, 63 | inverted: bool, 64 | width: u32, 65 | height: u32, 66 | ) -> Self { 67 | let display = ST7735 { 68 | spi, 69 | dc, 70 | rst, 71 | rgb, 72 | inverted, 73 | dx: 0, 74 | dy: 0, 75 | width, 76 | height, 77 | }; 78 | 79 | display 80 | } 81 | 82 | /// Runs commands to initialize the display. 83 | pub fn init(&mut self, delay: &mut DELAY) -> Result<(), ()> 84 | where 85 | DELAY: DelayNs, 86 | { 87 | self.hard_reset(delay)?; 88 | self.write_command(Instruction::SWRESET, &[])?; 89 | delay.delay_ms(200); 90 | self.write_command(Instruction::SLPOUT, &[])?; 91 | delay.delay_ms(200); 92 | self.write_command(Instruction::FRMCTR1, &[0x01, 0x2C, 0x2D])?; 93 | self.write_command(Instruction::FRMCTR2, &[0x01, 0x2C, 0x2D])?; 94 | self.write_command(Instruction::FRMCTR3, &[0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D])?; 95 | self.write_command(Instruction::INVCTR, &[0x07])?; 96 | self.write_command(Instruction::PWCTR1, &[0xA2, 0x02, 0x84])?; 97 | self.write_command(Instruction::PWCTR2, &[0xC5])?; 98 | self.write_command(Instruction::PWCTR3, &[0x0A, 0x00])?; 99 | self.write_command(Instruction::PWCTR4, &[0x8A, 0x2A])?; 100 | self.write_command(Instruction::PWCTR5, &[0x8A, 0xEE])?; 101 | self.write_command(Instruction::VMCTR1, &[0x0E])?; 102 | if self.inverted { 103 | self.write_command(Instruction::INVON, &[])?; 104 | } else { 105 | self.write_command(Instruction::INVOFF, &[])?; 106 | } 107 | if self.rgb { 108 | self.write_command(Instruction::MADCTL, &[0x00])?; 109 | } else { 110 | self.write_command(Instruction::MADCTL, &[0x08])?; 111 | } 112 | self.write_command(Instruction::COLMOD, &[0x05])?; 113 | self.write_command(Instruction::DISPON, &[])?; 114 | delay.delay_ms(200); 115 | Ok(()) 116 | } 117 | 118 | pub fn hard_reset(&mut self, delay: &mut DELAY) -> Result<(), ()> 119 | where 120 | DELAY: DelayNs, 121 | { 122 | if let Some(rst) = &mut self.rst { 123 | rst.set_high().map_err(|_| ())?; 124 | delay.delay_ms(10); 125 | rst.set_low().map_err(|_| ())?; 126 | delay.delay_ms(10); 127 | rst.set_high().map_err(|_| ())?; 128 | } 129 | Ok(()) 130 | } 131 | 132 | fn write_command(&mut self, command: Instruction, params: &[u8]) -> Result<(), ()> { 133 | self.dc.set_low().map_err(|_| ())?; 134 | self.spi.write(&[command as u8]).map_err(|_| ())?; 135 | if !params.is_empty() { 136 | self.start_data()?; 137 | self.write_data(params)?; 138 | } 139 | Ok(()) 140 | } 141 | 142 | fn start_data(&mut self) -> Result<(), ()> { 143 | self.dc.set_high().map_err(|_| ()) 144 | } 145 | 146 | fn write_data(&mut self, data: &[u8]) -> Result<(), ()> { 147 | self.spi.write(data).map_err(|_| ()) 148 | } 149 | 150 | /// Writes a data word to the display. 151 | fn write_word(&mut self, value: u16) -> Result<(), ()> { 152 | self.write_data(&value.to_be_bytes()) 153 | } 154 | 155 | fn write_words_buffered(&mut self, words: impl IntoIterator) -> Result<(), ()> { 156 | let mut buffer = [0; 32]; 157 | let mut index = 0; 158 | for word in words { 159 | let as_bytes = word.to_be_bytes(); 160 | buffer[index] = as_bytes[0]; 161 | buffer[index + 1] = as_bytes[1]; 162 | index += 2; 163 | if index >= buffer.len() { 164 | self.write_data(&buffer)?; 165 | index = 0; 166 | } 167 | } 168 | self.write_data(&buffer[0..index]) 169 | } 170 | 171 | pub fn set_orientation(&mut self, orientation: &Orientation) -> Result<(), ()> { 172 | if self.rgb { 173 | self.write_command(Instruction::MADCTL, &[*orientation as u8])?; 174 | } else { 175 | self.write_command(Instruction::MADCTL, &[*orientation as u8 | 0x08])?; 176 | } 177 | Ok(()) 178 | } 179 | 180 | /// Sets the global offset of the displayed image 181 | pub fn set_offset(&mut self, dx: u16, dy: u16) { 182 | self.dx = dx; 183 | self.dy = dy; 184 | } 185 | 186 | /// Sets the address window for the display. 187 | pub fn set_address_window(&mut self, sx: u16, sy: u16, ex: u16, ey: u16) -> Result<(), ()> { 188 | self.write_command(Instruction::CASET, &[])?; 189 | self.start_data()?; 190 | self.write_word(sx + self.dx)?; 191 | self.write_word(ex + self.dx)?; 192 | self.write_command(Instruction::RASET, &[])?; 193 | self.start_data()?; 194 | self.write_word(sy + self.dy)?; 195 | self.write_word(ey + self.dy) 196 | } 197 | 198 | /// Sets a pixel color at the given coords. 199 | pub fn set_pixel(&mut self, x: u16, y: u16, color: u16) -> Result<(), ()> { 200 | self.set_address_window(x, y, x, y)?; 201 | self.write_command(Instruction::RAMWR, &[])?; 202 | self.start_data()?; 203 | self.write_word(color) 204 | } 205 | 206 | /// Writes pixel colors sequentially into the current drawing window 207 | pub fn write_pixels>(&mut self, colors: P) -> Result<(), ()> { 208 | self.write_command(Instruction::RAMWR, &[])?; 209 | self.start_data()?; 210 | for color in colors { 211 | self.write_word(color)?; 212 | } 213 | Ok(()) 214 | } 215 | pub fn write_pixels_buffered>( 216 | &mut self, 217 | colors: P, 218 | ) -> Result<(), ()> { 219 | self.write_command(Instruction::RAMWR, &[])?; 220 | self.start_data()?; 221 | self.write_words_buffered(colors) 222 | } 223 | 224 | /// Sets pixel colors at the given drawing window 225 | pub fn set_pixels>( 226 | &mut self, 227 | sx: u16, 228 | sy: u16, 229 | ex: u16, 230 | ey: u16, 231 | colors: P, 232 | ) -> Result<(), ()> { 233 | self.set_address_window(sx, sy, ex, ey)?; 234 | self.write_pixels(colors) 235 | } 236 | 237 | pub fn set_pixels_buffered>( 238 | &mut self, 239 | sx: u16, 240 | sy: u16, 241 | ex: u16, 242 | ey: u16, 243 | colors: P, 244 | ) -> Result<(), ()> { 245 | self.set_address_window(sx, sy, ex, ey)?; 246 | self.write_pixels_buffered(colors) 247 | } 248 | 249 | /// Allows adjusting gamma correction on the display. 250 | /// 251 | /// Takes in an array `pos` for positive polarity correction and an array `neg` for negative polarity correction. 252 | /// 253 | /// The following values worked well on an ST7735S test device: 254 | /// pos: &[0x10, 0x0E, 0x02, 0x03, 0x0E, 0x07, 0x02, 0x07, 0x0A, 0x12, 0x27, 0x37, 0x00, 0x0D, 0x0E, 0x10] 255 | /// neg: &[0x10, 0x0E, 0x03, 0x03, 0x0F, 0x06, 0x02, 0x08, 0x0A, 0x13, 0x26, 0x36, 0x00, 0x0D, 0x0E, 0x10] 256 | pub fn adjust_gamma(&mut self, pos: &[u8;16], neg: &[u8;16]) -> Result<(), ()> { 257 | self.write_command(Instruction::GMCTRP1, pos)?; 258 | self.write_command(Instruction::GMCTRN1, neg) 259 | } 260 | } 261 | 262 | #[cfg(feature = "graphics")] 263 | extern crate embedded_graphics_core; 264 | #[cfg(feature = "graphics")] 265 | use self::embedded_graphics_core::{ 266 | draw_target::DrawTarget, 267 | pixelcolor::{ 268 | raw::{RawData, RawU16}, 269 | Rgb565, 270 | }, 271 | prelude::*, 272 | primitives::Rectangle, 273 | }; 274 | 275 | #[cfg(feature = "graphics")] 276 | impl DrawTarget for ST7735 277 | where 278 | SPI: spi::SpiDevice, 279 | DC: OutputPin, 280 | RST: OutputPin, 281 | { 282 | type Error = (); 283 | type Color = Rgb565; 284 | 285 | fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> 286 | where 287 | I: IntoIterator>, 288 | { 289 | for Pixel(coord, color) in pixels.into_iter() { 290 | // Only draw pixels that would be on screen 291 | if coord.x >= 0 292 | && coord.y >= 0 293 | && coord.x < self.width as i32 294 | && coord.y < self.height as i32 295 | { 296 | self.set_pixel( 297 | coord.x as u16, 298 | coord.y as u16, 299 | RawU16::from(color).into_inner(), 300 | )?; 301 | } 302 | } 303 | 304 | Ok(()) 305 | } 306 | 307 | fn fill_contiguous(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error> 308 | where 309 | I: IntoIterator, 310 | { 311 | // Clamp area to drawable part of the display target 312 | let drawable_area = area.intersection(&Rectangle::new(Point::zero(), self.size())); 313 | 314 | if drawable_area.size != Size::zero() { 315 | self.set_pixels_buffered( 316 | drawable_area.top_left.x as u16, 317 | drawable_area.top_left.y as u16, 318 | (drawable_area.top_left.x + (drawable_area.size.width - 1) as i32) as u16, 319 | (drawable_area.top_left.y + (drawable_area.size.height - 1) as i32) as u16, 320 | area.points() 321 | .zip(colors) 322 | .filter(|(pos, _color)| drawable_area.contains(*pos)) 323 | .map(|(_pos, color)| RawU16::from(color).into_inner()), 324 | )?; 325 | } 326 | 327 | Ok(()) 328 | } 329 | 330 | fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> { 331 | self.set_pixels_buffered( 332 | 0, 333 | 0, 334 | self.width as u16 - 1, 335 | self.height as u16 - 1, 336 | core::iter::repeat(RawU16::from(color).into_inner()) 337 | .take((self.width * self.height) as usize), 338 | ) 339 | } 340 | } 341 | 342 | #[cfg(feature = "graphics")] 343 | impl OriginDimensions for ST7735 344 | where 345 | SPI: spi::SpiDevice, 346 | DC: OutputPin, 347 | RST: OutputPin, 348 | { 349 | fn size(&self) -> Size { 350 | Size::new(self.width, self.height) 351 | } 352 | } 353 | --------------------------------------------------------------------------------